diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 00000000..9e1f8b92 --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +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 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..703134e3 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +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 + diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..f0598fd2 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +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 + mvn test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 285b7238..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: java -jdk: -- openjdk8 -- openjdk11 -- 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 088237e4..31e9571b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,170 @@ # 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) @@ -41,7 +204,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/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index bc4ba2b1..4e788d46 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,66 +1,114 @@ -List of 64 third-party dependencies (auto-generated on 2022-02-08 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.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) 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 new file mode 100644 index 00000000..67bf42f2 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +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` diff --git a/README.md b/README.md index e146e312..80a7df8a 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) +[![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) @@ -6,10 +6,9 @@ # Smart-ID Java client -This version of the library uses Smart-ID API v 2.0. +This library supports Smart-ID API v3.1. - -# Table of contents +## Table of contents * [Smart-ID Java client](#smart-id-java-client) * [Introduction](#introduction) @@ -17,39 +16,73 @@ This version of the library uses Smart-ID API v 2.0. * [Requirements](#requirements) * [Getting the library](#getting-the-library) * [Changelog](#changelog) -* [How to use it](#how-to-use-it) - * [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) + * [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) - * [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) - + ## 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 @@ -58,7 +91,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 21 ## Getting the library @@ -81,12 +114,23 @@ 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 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. -# How to use it +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://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 @@ -99,441 +143,1334 @@ 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 +## Setting up SmartIdClient for v3.1 -Smart-ID API returns the IP address of the user's device for subscribed Relying Parties who -ask it to be returned. +[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 -Requesting for the IP address to be returned: +### Setting up SSL connection to Smart-ID API -* [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() +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. -The returned info can be retrieved using one of: +```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()); -* [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() +// 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 -## Example of configuring the client +Also it is possible to add trusted certificates one by one. -You need a client for any call to API. +```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..."); +``` -The production environment host URL, Relying Party UUID and name are fixed in the Smart-ID service agreement. +## Device-link flows -### Verifying the SSL connection to Application Provider (SK) +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 -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). +### Device-link authentication session +#### Request parameters -#### Reading trusted certificates from key store +* `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. -It is recommended to keep trusted certificates in a trust store file: +#### Response parameters - -```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()); +* `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. -// 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); +#### 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. -### Feeding trusted certificates one by one +##### Initiating a device-link authentication session with semantics identifier -It also possible to feed trusted certificates one by one. -This can prove useful when trusted certificates are kept as application configuration property. +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 -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..." -); +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 -## Examples of performing authentication +```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. -### 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) +##### Initiating a device-link authentication session with document number for Web2App flow ```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) +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 -// For security reasons a new hash value must be created for each new authentication request -AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); +// 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: -String verificationCode = authenticationHash.calculateVerificationCode(); +* `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. -// NB! Display verification code to the customer for a few seconds before starting next step: +#### Examples of initiating a device-link signature session -SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() +##### 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) - .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(); + .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. -// You need this later to pull user's signing certificate -String documentNumberForFurtherReference = authenticationResponse.getDocumentNumber(); +##### Initiating a device-link signature session with document number -// We get IP of Smart-ID app since we made the request .withShareMdClientIpAddress(true) -String deviceIpAddress = authenticationResponse.getDeviceIpAddress(); +```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 -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. +#### Using request properties to request the IP address of the user's device -### Authenticating with document number +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 -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. +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. ```java -AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); +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 -String verificationCode = authenticationHash.calculateVerificationCode(); +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. -// NB! Display verification code to the customer for a few seconds before starting next step: +Below are examples of interaction elements specifically for device link flows: -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(); +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..") +)); +``` -## Validating authentication response +### Generating QR-code or device link -It is mandatory to validate the authentication response. -Validation performs following checks: +Documentation to device link and QR-code requirements +https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html -- 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. +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 -Validation returns information about the authenticated person. +#### Generating device link -```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(); +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 -// throws SmartIdResponseValidationException if validation doesn't pass -AuthenticationIdentity authIdentity = authenticationResponseValidator.validate(authenticationResponse); +* `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. -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 +```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()); ``` -### Extracting date-of-birth +##### 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()); +``` -Since all Estonian and Lithuanian national identity numbers contain date-of-birth -this getDateOfBirth function always returns a correct value for them. +#### Generating QR-code -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. +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. -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 +##### 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 ``` -Optional dateOfBirth = authIdentity.getDateOfBirth(); + +##### 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. -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. +### 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()); +} ``` -AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(signersCertificate); -Optional dateOfBirthExtracted = identity.getDateOfBirth(); + +#### 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 -## Creating a signature +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. -### Obtaining signer's certificate +#### Set up CertificateValidator -To create a digital signature, most format require the signer's certificate beforehand. -To fetch the certificate you can use documentNumber. +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 -SmartIdCertificate responseWithSigningCertificate = client - .getCertificate() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result - .withCertificateLevel("QUALIFIED") - .fetch(); +// 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(); -X509Certificate signersCertificate = responseWithSigningCertificate.getCertificate(); + +// 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); ``` -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 -(as each device has separate signing certificate). +#### Example of validating the authentication sessions response: -### Create the signature +##### Device link-based authentication session status validation -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. +DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) -You can send different interactions to user's device and it picks the first one that the app can handle. +```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"); +} +``` -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. +##### Notification-based authentication session status validation +NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) ```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="); +// 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()); +} +``` -// to display the verification code -String verificationCode = hashToSign.calculateVerificationCode(); +### 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 -// pause for a few seconds before starting following signing process +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; -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(); +// Build the certificate by document number request and query the certificate +CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); -byte[] signature = smartIdSignature.getValue(); +// Set up the certificate validator +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); -smartIdSignature.getInteractionFlowUsed(); // which interaction was used +// Validate the certificate +certificateValidator.validateCertificate(certResponse.certificate()); ``` +Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). -# Setting the order of preferred interactions for displaying text and asking PIN +## Linked signature flow -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. +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. -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. -* `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. +### Device link certificate choice session -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`. +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). -`displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. +#### Request Parameters -## Parameter allowedInteractionsOrder most common examples +* `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. -Following allowedInteractionsOrder combinations are most likely to be used. +#### Response parameters -### Short confirmation message with PIN +* `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. -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. +#### Example of initiating a device-link certificate choice session ```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(); +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. -### Verification code choice +#### Request parameters -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. +* `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. -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. +#### Response parameters -If user's app doesn't support displaying verification code choice then system falls back to displaying text and PIN input. +* `sessionID`: Required. String that can be used to request the signature session status result. + +#### Example of initiating a linked notification-based signature session ```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"); -} +// 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 -### Long confirmation message with fallback to PIN +```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(); -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. +// 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 -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(); +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); -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"); -} +// 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 -### Long confirmation message together with verification code choice with fallback to verification code choice +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); -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. +// Create the Semantics Identifier +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. +// 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 -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(); +// 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. -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."); +### 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 -### Interactions with longer text without fallback +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. -Relying Party can require interactions without fallback. -If End User's phone doesn't support required flow the library throws `RequiredInteractionNotSupportedByAppException`. +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 -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.") +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. )) - .sign(); -} -catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("User's Smart-ID app is not capable of displaying required interaction"); -} + // 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(); ``` -## 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. - +### 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: @@ -593,46 +1530,8 @@ 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/v2/"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); 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); -``` - -### 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); -``` - +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 180ff8eb..7e13153b 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,66 @@ 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 + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + - 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 +171,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.12 @@ -174,7 +199,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.3.1 attach-sources @@ -188,7 +213,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.1 + 3.10.0 attach-javadocs @@ -202,7 +227,7 @@ org.codehaus.mojo license-maven-plugin - 1.16 + 2.4.0 create-license-list @@ -233,7 +258,7 @@ org.owasp dependency-check-maven - 8.2.1 + 12.1.6 true false @@ -250,13 +275,16 @@ com.github.spotbugs spotbugs-maven-plugin - 3.1.12 + 4.8.6.4 + + false + org.apache.maven.plugins maven-jar-plugin - 3.2.2 + 3.4.2 diff --git a/private.key.enc b/private.key.enc deleted file mode 100644 index 9b877905..00000000 Binary files a/private.key.enc and /dev/null differ 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/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java new file mode 100644 index 00000000..f5f45506 --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -0,0 +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)); + } +} 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 6f3440f6..00000000 --- a/src/main/java/ee/sk/smartid/AuthenticationHash.java +++ /dev/null @@ -1,68 +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 java.security.SecureRandom; - -/** - * Class containing the hash and its hash type used for authentication - */ -public class AuthenticationHash extends SignableHash { - - /** - * 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; - } - - /** - * creates {@link AuthenticationHash} instance - * containing a randomly generated SHA-512 hash - * - * @return authentication hash - */ - public static AuthenticationHash generateRandomHash() { - return generateRandomHash(HashType.SHA512); - } - - private static byte[] getRandomBytes() { - byte[] randBytes = new byte[64]; - new SecureRandom().nextBytes(randBytes); - return randBytes; - } - -} diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index 420b053a..e2559dc5 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 @@ -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,98 +30,156 @@ 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; - - 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; - } - - /** - * 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; - } - - 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 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; + + /** + * 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 new file mode 100644 index 00000000..bff9b6d6 --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.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 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/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java deleted file mode 100644 index 42b85de2..00000000 --- a/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java +++ /dev/null @@ -1,384 +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.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.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; -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 ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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 ee.sk.smartid.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/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java new file mode 100644 index 00000000..25bdabba --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -0,0 +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; + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java new file mode 100644 index 00000000..eb40e98c --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -0,0 +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); +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java new file mode 100644 index 00000000..75639014 --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -0,0 +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()); + } +} 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 e295e8ea..00000000 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ /dev/null @@ -1,308 +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.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 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.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.*; - -import static java.util.Arrays.asList; - -/** - * 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(); - } - - /** - * 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. - * - * @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); - } - } - - 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.getSubjectDN() ,trustedCACertificate.getSubjectDN() ); - - return true; - } catch (GeneralSecurityException e) { - logger.debug("Error verifying signer's certificate: " + certificate.getSubjectDN() + " against CA certificate: " + trustedCACertificate.getSubjectDN(), 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; - } - - 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()); - } - else if (rdn.getType().equalsIgnoreCase("SURNAME")) { - identity.setSurname(rdn.getValue().toString()); - } - else if (rdn.getType().equalsIgnoreCase("SERIALNUMBER")) { - identity.setIdentityNumber(rdn.getValue().toString().split("-", 2)[1]); - } - else if (rdn.getType().equalsIgnoreCase("C")) { - identity.setCountry(rdn.getValue().toString()); - } - } - - identity.setDateOfBirth(getDateOfBirth(identity)); - - return identity; - } - catch (InvalidNameException e) { - logger.error("Error getting authentication identity from the certificate", e); - throw new SmartIdClientException("Error getting authentication identity from the certificate", e); - } - } - - public 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/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java new file mode 100644 index 00000000..ba61db1c --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -0,0 +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"); + } + } +} 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..cafdf002 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.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% + */ + +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 new file mode 100644 index 00000000..d9dce29a --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java @@ -0,0 +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; + } +} 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/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index b72be40f..d7373133 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/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 @@ -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,35 +26,52 @@ * #L% */ +import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +/** + * Representation of different signing certificate levels. + */ +public enum CertificateLevel { -public class CertificateLevel { + /** + * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. + */ + ADVANCED(1), - private String certificateLevel; + /** + * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. + */ + QUALIFIED(2), - private static final Map certificateLevels = new HashMap<>(); + /** + * Shortened alias for QUALIFIED level. + */ + QSCD(2); - static { - certificateLevels.put("ADVANCED", 1); - certificateLevels.put("QUALIFIED", 2); - } + private final int level; - public CertificateLevel(String certificateLevel) { - if (certificateLevel == null) { - throw new IllegalArgumentException("certificateLevel cannot be null"); + CertificateLevel(int level) { + this.level = level; } - this.certificateLevel = certificateLevel; - } - public boolean isEqualOrAbove(String certificateLevel) { - if (this.certificateLevel.equalsIgnoreCase(certificateLevel)) { - return true; + /** + * 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; } - else if (certificateLevels.get(certificateLevel) != null && certificateLevels.get(this.certificateLevel) != null) { - return certificateLevels.get(certificateLevel) <= certificateLevels.get(this.certificateLevel); + + /** + * 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)); } - return false; - } } 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/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java deleted file mode 100644 index 4525ec36..00000000 --- a/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java +++ /dev/null @@ -1,347 +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.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.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; -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 ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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/interupted 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/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java new file mode 100644 index 00000000..6ade1924 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -0,0 +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)); + } +} + 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..0e499c12 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateValidator.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.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 new file mode 100644 index 00000000..72dec7e1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java @@ -0,0 +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); + } + } +} 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/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java new file mode 100644 index 00000000..55496621 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -0,0 +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)); + } +} \ 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 new file mode 100644 index 00000000..a7451f7e --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java new file mode 100644 index 00000000..a9a3f26b --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -0,0 +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"); + } + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java new file mode 100644 index 00000000..423a189e --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.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 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 new file mode 100644 index 00000000..56687739 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -0,0 +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"); + } + } + +} \ 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 new file mode 100644 index 00000000..0f8448aa --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkType.java @@ -0,0 +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; + } +} diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java index 6613323e..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 @@ -26,20 +26,36 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; -public class DigestCalculator { +import ee.sk.smartid.exception.permanent.SmartIdClientException; - 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); +/** + * 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 new file mode 100644 index 00000000..bf74c745 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DigestInput.java @@ -0,0 +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(); +} diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java new file mode 100644 index 00000000..26f323c1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -0,0 +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()); + } + } +} 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..be68443e --- /dev/null +++ b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java @@ -0,0 +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); + } +} 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..984a5114 --- /dev/null +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -0,0 +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)); + } +} 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..f7e74a9d --- /dev/null +++ b/src/main/java/ee/sk/smartid/HashAlgorithm.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 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 new file mode 100644 index 00000000..67785957 --- /dev/null +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -0,0 +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"); + } + } +} 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..dad59496 --- /dev/null +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -0,0 +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)); + } +} 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..52132b42 --- /dev/null +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.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.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 new file mode 100644 index 00000000..0efb494e --- /dev/null +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -0,0 +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)); + } +} \ 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 new file mode 100644 index 00000000..b694ffb5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java new file mode 100644 index 00000000..09dab6f5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java new file mode 100644 index 00000000..ead4a53b --- /dev/null +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/QrCodeGenerator.java b/src/main/java/ee/sk/smartid/QrCodeGenerator.java new file mode 100644 index 00000000..4daed4e7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/QrCodeGenerator.java @@ -0,0 +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); + } +} 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/RpChallenge.java b/src/main/java/ee/sk/smartid/RpChallenge.java new file mode 100644 index 00000000..eefc7ac7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/RpChallenge.java @@ -0,0 +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); + } +} diff --git a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java new file mode 100644 index 00000000..d55d62d9 --- /dev/null +++ b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java @@ -0,0 +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; + } +} 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..81bc797f --- /dev/null +++ b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java @@ -0,0 +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; + } +} diff --git a/src/main/java/ee/sk/smartid/SessionType.java b/src/main/java/ee/sk/smartid/SessionType.java new file mode 100644 index 00000000..be0b8b0f --- /dev/null +++ b/src/main/java/ee/sk/smartid/SessionType.java @@ -0,0 +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; + } +} diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java index ae54b887..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 @@ -29,58 +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 tpye. SHA-512 is default. - *

- * {@link #calculateHash()} and - * {@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. */ -public class SignableData implements Serializable { - - private 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 record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - public byte[] calculateHash() { - return DigestCalculator.calculateDigest(dataToSign, hashType); - } + /** + * 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); + } - /** - * 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); - } + /** + * 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 void setHashType(HashType hashType) { - this.hashType = hashType; - } + /** + * 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); + } - public HashType getHashType() { - return hashType; - } + /** + * 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 902bdc09..9d18117a 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/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 @@ -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,58 +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 tpye. - *

- * {@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. */ -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 record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - public HashType getHashType() { - return hashType; - } + /** + * 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 void setHashType(HashType hashType) { - this.hashType = 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 boolean areFieldsFilled() { - return hashType != null && hash != null && hash.length > 0; - } + 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"); + } + } - /** - * 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 - * displayed on the phone matches with the one shown on the web page. - * - * @return the verification code - */ - public String calculateVerificationCode() { - return VerificationCodeCalculator.calculate(hash); - } + /** + * 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 new file mode 100644 index 00000000..f6a33872 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.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% + */ + +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 new file mode 100644 index 00000000..3c91ed2d --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.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; + +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 new file mode 100644 index 00000000..972c4704 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java @@ -0,0 +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); +} 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..1f07b2c3 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.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 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/exception/useraction/UserRefusedVerificationChoiceException.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java similarity index 77% rename from src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java rename to src/main/java/ee/sk/smartid/SignatureProtocol.java index 37411a53..4ee32e67 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.exception.useraction; +package ee.sk.smartid; /*- * #%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,8 +26,18 @@ * #L% */ -public class UserRefusedVerificationChoiceException extends UserRefusedException { - public UserRefusedVerificationChoiceException() { - super("User cancelled verificationCodeChoice screen"); - } +/** + * 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/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java deleted file mode 100644 index ad358c27..00000000 --- a/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java +++ /dev/null @@ -1,383 +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.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.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; -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 ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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 ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link ee.sk.smartid.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 ee.sk.smartid.SignableData#setHashType(HashType)} - * can be used to select the wanted hash type - * and the data is hashed for you. - * - * @param dataToSign dat 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/interupted 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/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java new file mode 100644 index 00000000..31e933d5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -0,0 +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; + } +} 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..63b85de4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -0,0 +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"); + } + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java b/src/main/java/ee/sk/smartid/SignatureValueValidator.java similarity index 56% rename from src/test/java/ee/sk/smartid/SmartIdSignatureTest.java rename to src/main/java/ee/sk/smartid/SignatureValueValidator.java index bc08c72a..5df475af 100644 --- a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidator.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,32 +26,26 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.junit.Test; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -public class SmartIdSignatureTest { +import java.security.cert.X509Certificate; - @Test - public void getSignatureValueInBase64() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("VGVyZSBNYWFpbG0="); - assertEquals("VGVyZSBNYWFpbG0=", signature.getValueInBase64()); - } +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - @Test - public void getSignatureValueInBytes() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("RGVkZ2Vob2c="); - assertArrayEquals("Dedgehog".getBytes(), signature.getValue()); - } +/** + * Interface for signature value validator. + */ +public interface SignatureValueValidator { - @Test(expected = UnprocessableSmartIdResponseException.class) - public void incorrectBase64StringShouldThrowException() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("äIsNotValidBase64Character"); - signature.getValue(); - } + /** + * 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 new file mode 100644 index 00000000..678f5f9d --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java deleted file mode 100644 index ede48dd9..00000000 --- a/src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java +++ /dev/null @@ -1,152 +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.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/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index dce120b9..a3ba1def 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/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 @@ -26,15 +26,6 @@ * #L% */ -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.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; @@ -49,243 +40,373 @@ 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; - - /** - * Gets an instance of the certificate request builder - * - * @return certificate request builder instance - */ - public CertificateRequestBuilder getCertificate() { - SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - CertificateRequestBuilder 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()); - SignatureRequestBuilder 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()); - AuthenticationRequestBuilder 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); - SessionStatusPoller 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); + 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); } - 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); + + /** + * 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); } - 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; + + /** + * 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; } - catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); + + /** + * 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); } - } - 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 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; } - } - public void setSmartIdConnector(SmartIdConnector smartIdConnector) { - this.connector = smartIdConnector; - } + /** + * Gets the UUID of the relying party + * + * @return UUID of the relying party + */ + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } - /** Use setTrustStore() instead - * @param trustStore Trust store to load certificates from. - */ - @Deprecated - public void loadSslCertificatesFromKeystore(KeyStore trustStore) { - this.setTrustStore(trustStore); - } + /** + * 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/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java deleted file mode 100644 index d2cb4acf..00000000 --- a/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java +++ /dev/null @@ -1,225 +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.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.*; -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 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 SmartIdConnector connector; - private 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/SmartIdSignature.java b/src/main/java/ee/sk/smartid/SmartIdSignature.java deleted file mode 100644 index 9fef3508..00000000 --- a/src/main/java/ee/sk/smartid/SmartIdSignature.java +++ /dev/null @@ -1,97 +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.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/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java new file mode 100644 index 00000000..b8482bb8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/TrailerField.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.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 new file mode 100644 index 00000000..c2ebc210 --- /dev/null +++ b/src/main/java/ee/sk/smartid/TrustedCACertStore.java @@ -0,0 +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(); +} diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index f911d24a..661658f5 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/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 @@ -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, HashType.SHA256); - 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/VerificationCodeType.java b/src/main/java/ee/sk/smartid/VerificationCodeType.java new file mode 100644 index 00000000..9f5b673f --- /dev/null +++ b/src/main/java/ee/sk/smartid/VerificationCodeType.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% + */ + +/** + * 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 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..41a03d63 --- /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.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 new file mode 100644 index 00000000..1bcc7150 --- /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.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 new file mode 100644 index 00000000..4063127d --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/InteractionType.java @@ -0,0 +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(); +} 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..c56e52ae --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -0,0 +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(); + } +} 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/certificate/NonQualifiedSmartIdCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java new file mode 100644 index 00000000..5dbf7389 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java @@ -0,0 +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"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java new file mode 100644 index 00000000..716045ab --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java @@ -0,0 +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; + } +} 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/common/devicelink/interactions/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java new file mode 100644 index 00000000..29803eee --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java @@ -0,0 +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); + } +} + 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 new file mode 100644 index 00000000..2c1c1bda --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java @@ -0,0 +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; + } +} 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..7981f9fb --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java @@ -0,0 +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); + } +} 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..80f88363 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java @@ -0,0 +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; + } +} 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 new file mode 100644 index 00000000..5736ac83 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java @@ -0,0 +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); + } +} 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 new file mode 100644 index 00000000..00372c2f --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java @@ -0,0 +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"); + } +} 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..bb262cd7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java @@ -0,0 +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."); + } +} 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 new file mode 100644 index 00000000..3cf49946 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java @@ -0,0 +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); + } +} 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..b0d84fe5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java @@ -0,0 +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"); + } +} 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..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 @@ -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,26 @@ * #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 dc2010cf..2df63e82 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 @@ -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 DOCUMENT_UNUSABLE. + */ public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { + + /** + * Constructs the exception with default message. + */ 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/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 b1a55633..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 @@ -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,16 @@ 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 allowedInteractionsOrder interactions."); + 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 new file mode 100644 index 00000000..62e99b77 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java @@ -0,0 +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"); + } +} 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/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..4a1c2fd3 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/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 @@ -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,60 +26,84 @@ * #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.rest.dao.SessionStatus; +import java.util.concurrent.TimeUnit; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.TimeUnit; +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 SmartIdConnector connector; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; + private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - public SessionStatusPoller(SmartIdConnector connector) { - this.connector = connector; - } + private final SmartIdConnector connector; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; - 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); + /** + * 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; } - } - 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); + /** + * 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); + } } - logger.debug("Got session final session status response"); - return sessionStatus; - } - private SessionStatus pollSessionStatus(String sessionId) { - logger.debug("Polling session status"); - return connector.getSessionStatus(sessionId); - } + 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; + } - public void setPollingSleepTime(TimeUnit unit, long timeout) { - logger.debug("Polling sleep time is " + timeout + " " + unit.toString()); - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; - } + /** + * 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 75a94e8f..37ad21a8 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/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 @@ -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,31 +26,172 @@ * #L% */ -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.*; - -import javax.net.ssl.SSLContext; 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 { - SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; + /** + * 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); - CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request); + /** + * 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); - CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, CertificateRequest request); + /** + * 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); - SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request); + /** + * 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); - SignatureSessionResponse sign(SemanticsIdentifier identifier, SignatureSessionRequest request); + /** + * Set the SSL context to use for secure communication + * + * @param sslContext The SSL context + */ + void setSslContext(SSLContext sslContext); - AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request); + /** + * 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); - AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, AuthenticationSessionRequest request); + /** + * 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); - void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); + /** + * 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); - void setSslContext(SSLContext sslContext); + /** + * 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 bb6bdcf9..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 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,6 +26,17 @@ * #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; @@ -33,8 +44,29 @@ 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 jakarta.ws.rs.*; +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; @@ -42,273 +74,338 @@ 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.net.URI; -import java.util.concurrent.TimeUnit; +/** + * Smart-ID REST connector implementation. + */ +public class SmartIdRestConnector implements SmartIdConnector { -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + @Serial + private static final long serialVersionUID = 2025_09_10L; -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 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; - 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 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) { - 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(); + /** + * 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; } - } - - @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(); + /** + * 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; } - else { - client = this.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); } - 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]; + @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); } - catch (Exception e) { - return "-"; + + 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); } - } - - 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); + + @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); } - } - - 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); + + @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); } - } - - 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); + + @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); } - } - private T postRequest(URI uri, V request, Class responseType) { - try { - Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); - return prepareClient(uri).post(requestEntity, responseType); + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; } - catch (NotAuthorizedException e) { - logger.warn("Request is unauthorized for URI " + uri, e); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); + + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; } - catch (BadRequestException e) { - logger.warn("Request is invalid for URI " + uri, e); - throw new SmartIdClientException("Server refused the request", e); + + /** + * 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()); } - 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; + + /** + * 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() + ")"; } - catch (ServerErrorException e) { - if (e.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", e); - throw new ServerMaintenanceException(); - } - throw e; + + /** + * 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 SessionStatusRequest createSessionStatusRequest(String sessionId) { - SessionStatusRequest request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + 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; + } } - 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); + + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + var request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; } - } - @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } -} + 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/InteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java similarity index 62% rename from src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java rename to src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index 300d9263..b2bf44c4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.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 @@ -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,28 +26,16 @@ * #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 String code; - - InteractionFlow(String code) { - this.code = code; - } - - @JsonValue - public String getCode() { - return code; - } - - public boolean is(String typeCodeString) { - return this.getCode().equals(typeCodeString); - } +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 bb1401e7..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ /dev/null @@ -1,136 +0,0 @@ -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.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/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java new file mode 100644 index 00000000..443d2177 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.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.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 new file mode 100644 index 00000000..55b68605 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -0,0 +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 { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java deleted file mode 100644 index c6bf6417..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java +++ /dev/null @@ -1,100 +0,0 @@ -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 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/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java new file mode 100644 index 00000000..9bc32de6 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -0,0 +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 { +} 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/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/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java new file mode 100644 index 00000000..55b0f039 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -0,0 +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()); + } +} 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/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index 6653621c..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 - 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 @@ -27,107 +27,15 @@ */ import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; - -import static ee.sk.smartid.rest.dao.InteractionFlow.*; - -@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; - 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()); - } - } - } +/** + * 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 new file mode 100644 index 00000000..ebad4ab8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java @@ -0,0 +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) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/Capability.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java similarity index 77% rename from src/main/java/ee/sk/smartid/rest/dao/Capability.java rename to src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java index 38054fb0..eede94a4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Capability.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.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,6 +26,13 @@ * #L% */ -public enum Capability { - QUALIFIED, ADVANCED +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 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/SignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java similarity index 82% rename from src/main/java/ee/sk/smartid/rest/dao/SignatureSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java index 168e885b..1cb13f47 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.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,20 +26,16 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.io.Serializable; -@JsonIgnoreProperties(ignoreUnknown = true) -public class SignatureSessionResponse implements Serializable { - - private String sessionID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - public String getSessionID() { - return sessionID; - } +/** + * Notification-based authentication session response + * + * @param sessionID the ID of the created authentication session + */ - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } +@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 new file mode 100644 index 00000000..a665abde --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java @@ -0,0 +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 { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java similarity index 81% rename from src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java index ffd4dfe0..77e943ea 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.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 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,20 +26,15 @@ * #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; - } +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } +/** + * 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 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 new file mode 100644 index 00000000..9085d2d1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.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; + +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 new file mode 100644 index 00000000..16e9fe9b --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -0,0 +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 { +} \ 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 b607ee9c..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,26 +1,39 @@ package ee.sk.smartid.rest.dao; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; +/*- + * #%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 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; - } +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 b0527e18..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 { - protected 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 0071fa49..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 @@ -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,29 +26,55 @@ * #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; + private String value; + private String certificateLevel; - public String getValue() { - return value; - } + /** + * Get the certificate value. + * + * @return the certificate data in Base64-encoded format + */ + public String getValue() { + return value; + } - public void setValue(String value) { - this.value = value; - } + /** + * Set the certificate value. + * + * @param value the certificate data in Base64-encoded format + */ + public void setValue(String value) { + this.value = value; + } - public String getCertificateLevel() { - return certificateLevel; - } + /** + * Gets the certificate level. + * + * @return the certificate level + */ + public String getCertificateLevel() { + return certificateLevel; + } - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = 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 new file mode 100644 index 00000000..241378f5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java @@ -0,0 +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; + } +} 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..029782ba --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.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; + +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 1a6608dd..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 @@ -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,29 +26,76 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - 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 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; + } - public String getDocumentNumber() { - return documentNumber; - } + /** + * Get document number of the user used in the session. + * + * @return document number of the user + */ + public String getDocumentNumber() { + return documentNumber; + } - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } + /** + * Set document number of the user + * + * @param documentNumber document number of the user + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } - public String getEndResult() { - return endResult; - } + /** + * Get additional details + * + * @return details of the session result + */ + public SessionResultDetails getDetails() { + return details; + } - public void setEndResult(String endResult) { - this.endResult = endResult; - } + /** + * 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 new file mode 100644 index 00000000..26e82578 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java @@ -0,0 +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; + } +} 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 a16affbc..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 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,30 +26,135 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - 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 algorithm; + 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; + } - private String value; + /** + * Set the flow type. + * + * @param flowType the flow type + */ + public void setFlowType(String flowType) { + this.flowType = flowType; + } - public String getAlgorithm() { - return algorithm; - } + /** + * Get the signature algorithm. + * + * @return the signature algorithm + */ + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } - public void setAlgorithm(String algorithm) { - this.algorithm = algorithm; - } + /** + * Set the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + */ + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } - public String getValue() { - return value; - } + /** + * Get the signature algorithm parameters. + * + * @return the signature algorithm parameters + */ + public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } - public void setValue(String value) { - this.value = value; - } + /** + * 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 new file mode 100644 index 00000000..5fbcee83 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java @@ -0,0 +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; + } +} 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..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 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,88 +26,175 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.io.Serializable; -import java.util.Arrays; +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 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; - } - + 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 1c3c0e0e..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 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/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java similarity index 75% rename from src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java rename to src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index 4b5a5d1a..97ce547e 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.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,13 +26,13 @@ * #L% */ -import org.junit.Test; +import java.io.Serializable; -public class SignatureSessionRequestTest { - - @Test(expected = UnsupportedOperationException.class) - public void setDisplayText() { - SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); - signatureSessionRequest.setDisplayText("test"); - } +/** + * 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/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java deleted file mode 100644 index 81df1f66..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ /dev/null @@ -1,136 +0,0 @@ -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 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/rest/dao/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java similarity index 81% rename from src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index bc33e261..db9ff91d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.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,20 +26,16 @@ * #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; - } +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } +/** + * 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 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/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index 50be337e..c946a88c 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 @@ -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; @@ -41,20 +34,53 @@ 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 { -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). - * + *

* 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 +88,69 @@ 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())); + } + /** + * 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) { + } 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 +171,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(); @@ -121,5 +200,4 @@ private static ASN1Primitive toDerObject(byte[] data) throws IOException { 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 new file mode 100644 index 00000000..2ba91cbd --- /dev/null +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -0,0 +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(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index 5453ec45..64ac36e7 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 @@ -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,17 +26,24 @@ * #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; 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") @@ -44,55 +51,48 @@ 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(); - 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; + }; } + /** + * 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); - 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); @@ -101,9 +101,18 @@ 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 ("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 +120,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 +134,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/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 5d63daa8..6274c4e8 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 @@ -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,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.length() > 0; + 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.length() == 0; + 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_NQ_2021E.der.crt b/src/main/resources/trusted_certificates/EID_NQ_2021E.der.crt new file mode 100644 index 00000000..2a28306c Binary files /dev/null and b/src/main/resources/trusted_certificates/EID_NQ_2021E.der.crt differ diff --git a/src/main/resources/trusted_certificates/EID_NQ_2021R.der.crt b/src/main/resources/trusted_certificates/EID_NQ_2021R.der.crt new file mode 100644 index 00000000..f8118354 Binary files /dev/null and b/src/main/resources/trusted_certificates/EID_NQ_2021R.der.crt differ 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 00000000..2503b7e9 Binary files /dev/null and b/src/main/resources/trusted_certificates/EID_Q_2024E.der.crt differ diff --git a/src/main/resources/trusted_certificates/EID_Q_2024R.der.crt b/src/main/resources/trusted_certificates/EID_Q_2024R.der.crt new file mode 100644 index 00000000..06ce9bf4 Binary files /dev/null and b/src/main/resources/trusted_certificates/EID_Q_2024R.der.crt differ diff --git a/src/main/java/ee/sk/smartid/SmartIdCertificate.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java similarity index 53% rename from src/main/java/ee/sk/smartid/SmartIdCertificate.java rename to src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java index 4887b134..2da25525 100644 --- a/src/main/java/ee/sk/smartid/SmartIdCertificate.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.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,46 +26,29 @@ * #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; +import static org.junit.jupiter.api.Assertions.assertEquals; - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - public X509Certificate getCertificate() { - return certificate; - } - - public String getDocumentNumber() { - return documentNumber; - } +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } +import org.junit.jupiter.api.Test; - public String getCertificateLevel() { - return certificateLevel; - } +class AuthenticationIdentityMapperTest { - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } + @Test + void from() { + X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); + AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); - public String getDeviceIpAddress() { - return deviceIpAddress; - } + 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 49b583d7..c18f84dd 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,13 @@ * #L% */ -import org.junit.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -public class AuthenticationIdentityTest { - - @SuppressWarnings( "deprecation" ) - @Test - public void setSurName() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setSurName("surname1"); +import org.junit.jupiter.api.Test; - assertThat(authenticationIdentity.getSurname(), is("surname1")); - } - - @SuppressWarnings( "deprecation" ) - @Test - public void getSurName() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setSurname("surname"); - - assertThat(authenticationIdentity.getSurName(), is("surname")); - } +public class AuthenticationIdentityTest { @Test public void getIdentityCode() { @@ -66,6 +49,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 deleted file mode 100644 index 8f85fa66..00000000 --- a/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java +++ /dev/null @@ -1,621 +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.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 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.*; - -public class AuthenticationRequestBuilderTest { - - 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(); - } - -} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java new file mode 100644 index 00000000..a671136a --- /dev/null +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -0,0 +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; + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java deleted file mode 100644 index 3b18f8c2..00000000 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ /dev/null @@ -1,367 +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.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 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; - -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="; - - public static final String AUTH_CERTIFICATE_EE = "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"; - public static final String AUTH_CERTIFICATE_LV = "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"; - 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 = "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"; - 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="; - - private static final String HASH_TO_SIGN_IN_BASE64 = "pcWJTcOvmk5Xcvyfrit9SF55S3qU+NfEEVxg4fVf+GdxMN0W2wSpJVivcf91IG+Ji3aCGlNN8p5scBEn6mgUOg=="; - - private AuthenticationResponseValidator validator; - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @Before - 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() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Failed to verify validity of signature returned by Smart-ID")); - - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setSignatureValueInBase64("invalid"); - - validator.validate(response); - } - - @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() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Smart-ID API returned end result code 'NOT OK'")); - - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidEndResult(); - validator.validate(response); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignatureVerificationFails() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Failed to verify validity of signature returned by Smart-ID")); - - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidSignature(); - validator.validate(response); - } - - @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()); - - 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 = "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"; - 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); - } - 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()); - } - - } - } -} 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..dcc14ced --- /dev/null +++ b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.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.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 new file mode 100644 index 00000000..3634acfa --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.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.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 = "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"; + 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 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/CertificateLevelTest.java b/src/test/java/ee/sk/smartid/CertificateLevelTest.java deleted file mode 100644 index 45c6aa1b..00000000 --- a/src/test/java/ee/sk/smartid/CertificateLevelTest.java +++ /dev/null @@ -1,79 +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 org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -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(expected = IllegalArgumentException.class) - public void certificateLevel_nullArgumentToConstructor() { - 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..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 @@ -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 deleted file mode 100644 index b8621ae9..00000000 --- a/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java +++ /dev/null @@ -1,316 +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.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.*; - -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(); - } - -} diff --git a/src/test/java/ee/sk/smartid/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java new file mode 100644 index 00000000..dc96b6c7 --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -0,0 +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); + } +} 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..a3e29fa7 --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java @@ -0,0 +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()); + } +} diff --git a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java index 2824bf03..8b8590a2 100644 --- a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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 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/DefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java new file mode 100644 index 00000000..485f5d81 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.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 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 new file mode 100644 index 00000000..1f1ba798 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java @@ -0,0 +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)); + } +} 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..e5e007a2 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -0,0 +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") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java new file mode 100644 index 00000000..cdd8f4ec --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -0,0 +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])); + } +} 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..91c63e40 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -0,0 +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") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java new file mode 100644 index 00000000..4d1cb1e2 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -0,0 +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") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index d0a229e3..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 @@ -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,49 +26,54 @@ * #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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; -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 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 { - 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); + @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")); - } - - @Test - public void calculateDigest_sha512() { - byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA512); - - assertThat( Hex.encodeHexString(sha512), - is("861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8")); - } - - - @Test(expected = UnprocessableSmartIdResponseException.class) public void calculateDigest_nullHashType() { - DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null); - + 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/DummyData.java b/src/test/java/ee/sk/smartid/DummyData.java deleted file mode 100644 index a4b59658..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 String CERTIFICATE = "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"; - - 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/DuplicateDeviceLinkInteractionsProvider.java b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java new file mode 100644 index 00000000..36285640 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.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.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 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/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java deleted file mode 100644 index 31c0df0e..00000000 --- a/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java +++ /dev/null @@ -1,329 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%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 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; - -; - -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"; - - 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" - + "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\n" - + "nUMrQKgVkBFH6CVchyHXPejDNGA=\n" - + "-----END CERTIFICATE-----"; - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @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(); - } - - @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() 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(); - } - - @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(); - } - - @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() 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(); - } - - @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(); - } - -} diff --git a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java new file mode 100644 index 00000000..4e48ca24 --- /dev/null +++ b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java @@ -0,0 +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())); + } +} 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/FileUtil.java b/src/test/java/ee/sk/smartid/FileUtil.java new file mode 100644 index 00000000..20ad20ae --- /dev/null +++ b/src/test/java/ee/sk/smartid/FileUtil.java @@ -0,0 +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); + } + } +} 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..e3fc3a35 --- /dev/null +++ b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java @@ -0,0 +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); + } + } + } +} 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 new file mode 100644 index 00000000..2e817836 --- /dev/null +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -0,0 +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?"))); + } +} 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..26622f2c --- /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.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 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 new file mode 100644 index 00000000..dab26384 --- /dev/null +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -0,0 +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") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java new file mode 100644 index 00000000..94e9cb1c --- /dev/null +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -0,0 +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") + ); + } + } +} \ 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 new file mode 100644 index 00000000..55bbf052 --- /dev/null +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -0,0 +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); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java new file mode 100644 index 00000000..f0c4560c --- /dev/null +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -0,0 +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()); + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeUtil.java b/src/test/java/ee/sk/smartid/QrCodeUtil.java new file mode 100644 index 00000000..5039bd55 --- /dev/null +++ b/src/test/java/ee/sk/smartid/QrCodeUtil.java @@ -0,0 +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); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java new file mode 100644 index 00000000..2069e9b9 --- /dev/null +++ b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java @@ -0,0 +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); + } + } +} diff --git a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java new file mode 100644 index 00000000..79ddd848 --- /dev/null +++ b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java @@ -0,0 +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()); + } +} diff --git a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java new file mode 100644 index 00000000..4eb6df4e --- /dev/null +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -0,0 +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) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java index 531abfca..82d57a2d 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 @@ -26,45 +26,43 @@ * #L% */ -import org.apache.commons.codec.binary.Base64; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import java.util.Base64; -public class SignableDataTest { +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; - 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="; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - @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()); - } +class SignableDataTest { - @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()); - } + private static final byte[] TEST_DATA = "Test data".getBytes(); - @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 + 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 c0e93280..7d6bdf7c 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -26,24 +26,39 @@ * #L% */ -import org.junit.Assert; -import org.junit.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 calculateVerificationCodeWithSha512() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA512); - hashToSign.setHash(DigestCalculator.calculateDigest("Hello World!".getBytes(), HashType.SHA512)); - Assert.assertEquals("4664", hashToSign.calculateVerificationCode()); - } +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/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java deleted file mode 100644 index 0f44e1c6..00000000 --- a/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java +++ /dev/null @@ -1,536 +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.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.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.*; - -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(); - } - -} 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..5f7d52d2 --- /dev/null +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -0,0 +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; + } +} 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..4ab8d7ac --- /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 = "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"; + 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/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java deleted file mode 100644 index f5705810..00000000 --- a/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java +++ /dev/null @@ -1,66 +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.exception.UnprocessableSmartIdResponseException; -import org.apache.commons.codec.binary.Base64; -import org.junit.Test; - -import java.security.cert.CertificateEncodingException; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -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(expected = UnprocessableSmartIdResponseException.class) - public void incorrectBase64StringShouldThrowException() { - 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/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 425ae613..a44def42 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,1101 +26,786 @@ * #L% */ -import com.github.tomakehurst.wiremock.junit.WireMockRule; +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.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.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.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.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; - -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()); - } - +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 new file mode 100644 index 00000000..5bfbd684 --- /dev/null +++ b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java @@ -0,0 +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"); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java new file mode 100644 index 00000000..14a5fc74 --- /dev/null +++ b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java @@ -0,0 +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 { +} diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 1fcec122..66b6118b 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 @@ -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,128 @@ * #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 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))) - .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))) - .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) - ); - } - - 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 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); } - catch (IOException e) { - throw new RuntimeException(e); + + 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/main/java/ee/sk/smartid/HashType.java b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java similarity index 51% rename from src/main/java/ee/sk/smartid/HashType.java rename to src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java index 89d02a3e..47e0860d 100644 --- a/src/main/java/ee/sk/smartid/HashType.java +++ b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.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,31 +26,23 @@ * #L% */ -public enum HashType { +import java.util.stream.Stream; - 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 }); +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; - private String algorithmName; - private String hashTypeName; - private byte[] digestInfoPrefix; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; - HashType(String algorithmName, String hashTypeName, byte[] digestInfoPrefix) { - this.algorithmName = algorithmName; - this.hashTypeName = hashTypeName; - this.digestInfoPrefix = digestInfoPrefix.clone(); - } +public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { - public String getAlgorithmName() { - return algorithmName; - } - - public String getHashTypeName() { - return hashTypeName; - } - - public byte[] getDigestInfoPrefix() { - return digestInfoPrefix.clone(); - } + @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 14ab494f..847e2fa7 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,58 @@ * #L% */ -import org.junit.Test; + +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; -import static org.junit.Assert.assertEquals; 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 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 new file mode 100644 index 00000000..7f26c056 --- /dev/null +++ b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -0,0 +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); + } +} 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..15aadb63 --- /dev/null +++ b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -0,0 +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); + } +} 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/UrlSafeTokenGeneratorTest.java b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java new file mode 100644 index 00000000..69adad3c --- /dev/null +++ b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java @@ -0,0 +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()); + } +} 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/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java similarity index 92% rename from src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java rename to src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java index bfea9fe0..4f5a2031 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.dao; /*- * #%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,11 +26,14 @@ * #L% */ -import org.junit.Test; 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 diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java new file mode 100644 index 00000000..b3619a6f --- /dev/null +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -0,0 +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); + } + } +} diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java new file mode 100644 index 00000000..69ba46f4 --- /dev/null +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -0,0 +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 + ); + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java index 175a3515..b50ab0a9 100644 --- a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,58 @@ * #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.*; - -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++); - } - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } +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; - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - return null; - } +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, - CertificateRequest request) { - return null; - } +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.SessionStatus; - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - return null; - } +class SessionStatusPollerTest { - @Override - public SignatureSessionResponse sign(SemanticsIdentifier identifier, - SignatureSessionRequest request) { - return null; - } + private SmartIdConnector smartIdConnector; - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - return null; - } + private SessionStatusPoller poller; - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, - AuthenticationSessionRequest request) { - return null; + @BeforeEach + void setUp() { + smartIdConnector = mock(SmartIdConnector.class); + poller = new SessionStatusPoller(smartIdConnector); } - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - SessionStatusRequest request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; + @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()); } - @Override - public void setSslContext(SSLContext sslContext) { + @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/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/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index db590eb2..0082e0ee 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,1754 @@ * #L% */ -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import ee.sk.smartid.ClientRequestHeaderFilter; +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.*; -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 java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; +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); -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.*; - -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; - } + 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/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java deleted file mode 100644 index 6008d599..00000000 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java +++ /dev/null @@ -1,275 +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.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 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; - -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; - - @Before - public void setUp() { - connector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v2/"); - - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); - } - - @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(isEmptyOrNullString())); - - 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(isEmptyOrNullString())); - - 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(isEmptyOrNullString())); - - 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(isEmptyOrNullString())); - - 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(isEmptyOrNullString())); - 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(isEmptyOrNullString())); - 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(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); - } - -} 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/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index ef899ca8..3e742f4e 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,42 @@ * #L% */ -import org.junit.Test; +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 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.smartid.CertificateUtil; +import ee.sk.smartid.InvalidCertificateGenerator; public class CertificateAttributeUtilTest { + private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "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"; + 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.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); @@ -50,11 +71,72 @@ public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws @Test public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { - X509Certificate certificateWithoutDobAttribute = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LV)); + 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 966d9ebe..47e53d1f 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/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 @@ -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,29 +26,36 @@ * #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.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 = "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"; + private static final String AUTH_CERTIFICATE_EE = "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"; + private static final String AUTH_CERTIFICATE_LT = "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"; + @Test public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { + X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); - X509Certificate eeCertificate = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_EE)); - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(eeCertificate); - + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); @@ -58,9 +65,9 @@ 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.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(lvCertificate); + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); @@ -70,9 +77,9 @@ 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.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(ltCertificate); + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); @@ -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 deleted file mode 100644 index b83ef864..00000000 --- a/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java +++ /dev/null @@ -1,749 +0,0 @@ -package ee.sk.test.smartid.integration; - -/*- - * #%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 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 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 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; - -/** - * 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! - */ -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 - 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="); - - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); - } - - /* - - ## 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 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) - 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..." - ); - } - - /* - - ## 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(expected = UnprocessableSmartIdResponseException.class) - 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 = "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"; - 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 of 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`. First screen is for text only (max 200 chars) and has Confirm and Cancel buttons. Second screen is for 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`. - -`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 documentApacheHttpCleint() { - 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 - @Ignore("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, "030303-10012"); - - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); - - 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") - 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, "030303-10012"); - - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); - - 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 deleted file mode 100644 index b781ccbe..00000000 --- a/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java +++ /dev/null @@ -1,308 +0,0 @@ -package ee.sk.test.smartid.integration; - -/*- - * #%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.*; -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 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; - -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 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" - + "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\n" - + "nUMrQKgVkBFH6CVchyHXPejDNGA=\n" - + "-----END CERTIFICATE-----"; - - - @Before - 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); - - // temporary solution to skip tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); - } - - @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("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")); - } - - @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, "030403-10075")) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-030403-10075-ZH4M-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("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")); - } - - @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("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")); - - 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("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")); - - 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(DOCUMENT_NUMBER) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039914-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("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")); - } - - - @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(DOCUMENT_NUMBER) - .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("30303039914")); - assertThat(authenticationIdentity.getCountry(), is("LT")); - - System.out.println("Device IP: " + authenticationResponse.getDeviceIpAddress()); - } - - private void assertSignatureCreated(SmartIdSignature signature) { - assertNotNull(signature); - assertThat(signature.getValueInBase64(), not(isEmptyOrNullString())); - } - - private void assertCertificateChosen(SmartIdCertificate certificateResponse) { - assertNotNull(certificateResponse); - assertThat(certificateResponse.getDocumentNumber(), not(isEmptyOrNullString())); - assertNotNull(certificateResponse.getCertificate()); - } - - private void assertAuthenticationResponseCreated(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) { - assertNotNull(authenticationResponse); - assertThat(authenticationResponse.getEndResult(), not(isEmptyOrNullString())); - assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); - assertThat(authenticationResponse.getSignatureValueInBase64(), not(isEmptyOrNullString())); - 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 baeb8985..3090342a 100644 Binary files a/src/test/resources/demo_server_trusted_ssl_certs.jks and b/src/test/resources/demo_server_trusted_ssl_certs.jks differ 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 de52d7e3..00000000 Binary files a/src/test/resources/demo_server_trusted_ssl_certs.p12 and /dev/null differ 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..58621ea6 --- /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-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 new file mode 100644 index 00000000..0ccbe990 --- /dev/null +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json @@ -0,0 +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=" +} \ 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..2f6ed5ce --- /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-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 new file mode 100644 index 00000000..4a0177cb --- /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-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 new file mode 100644 index 00000000..683dbb84 --- /dev/null +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json @@ -0,0 +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" +} 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..22276dab --- /dev/null +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json @@ -0,0 +1,4 @@ +{ + "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 new file mode 100644 index 00000000..5fd36502 --- /dev/null +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json @@ -0,0 +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" +} \ No newline at end of file diff --git a/src/test/resources/requests/authenticationSessionRequest.json b/src/test/resources/requests/authenticationSessionRequest.json deleted file mode 100644 index 83c5676a..00000000 --- a/src/test/resources/requests/authenticationSessionRequest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", - "hashType": "SHA512", - "allowedInteractionsOrder": [ - {"type": "confirmationMessageAndVerificationCodeChoice", "displayText200":"Log in to self-service?"}, - {"type": "displayTextAndPIN", "displayText60": "Log in?"} - ] -} diff --git a/src/test/resources/requests/authenticationSessionRequestWithNonce.json b/src/test/resources/requests/authenticationSessionRequestWithNonce.json deleted file mode 100644 index 6536712c..00000000 --- a/src/test/resources/requests/authenticationSessionRequestWithNonce.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", - "hashType": "SHA512", - "nonce": "g9rp4kjca3", - "allowedInteractionsOrder": [ - {"type": "confirmationMessageAndVerificationCodeChoice", "displayText200":"Log in to self-service?"}, - {"type": "displayTextAndPIN","displayText60": "Log in?"} - ] -} diff --git a/src/test/resources/requests/authenticationSessionRequestWithSingleAllowedInteraction.json b/src/test/resources/requests/authenticationSessionRequestWithSingleAllowedInteraction.json deleted file mode 100644 index d94fa367..00000000 --- a/src/test/resources/requests/authenticationSessionRequestWithSingleAllowedInteraction.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", - "hashType": "SHA512", - "allowedInteractionsOrder": [ - { - "type": "displayTextAndPIN", - "displayText60": "Log into internet banking system" - } - ] -} diff --git a/src/test/resources/requests/certificateChoiceRequest.json b/src/test/resources/requests/certificateChoiceRequest.json deleted file mode 100644 index bcba473f..00000000 --- a/src/test/resources/requests/certificateChoiceRequest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED" -} \ No newline at end of file diff --git a/src/test/resources/requests/certificateChoiceRequestWithNonce.json b/src/test/resources/requests/certificateChoiceRequestWithNonce.json deleted file mode 100644 index f6dc3238..00000000 --- a/src/test/resources/requests/certificateChoiceRequestWithNonce.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "nonce": "zstOt2umlc" -} \ 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 new file mode 100644 index 00000000..f1d018f4 --- /dev/null +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000..39c99d9e --- /dev/null +++ b/src/test/resources/requests/sign/certificate-by-document-number-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/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/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 new file mode 100644 index 00000000..b980efff --- /dev/null +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..00ceb1cb --- /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-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 new file mode 100644 index 00000000..16cf47ee --- /dev/null +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 00000000..4c8d933b --- /dev/null +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..f1d018f4 --- /dev/null +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000..7c17de75 --- /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-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 new file mode 100644 index 00000000..0b2cc34d --- /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-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 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/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/requests/signatureSessionRequest.json b/src/test/resources/requests/signatureSessionRequest.json deleted file mode 100644 index 7b3731ec..00000000 --- a/src/test/resources/requests/signatureSessionRequest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - {"type": "confirmationMessage", "displayText200":"Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"}, - {"type": "displayTextAndPIN","displayText60": "Transfer 1 unit to account 7677323232?"} - ] -} diff --git a/src/test/resources/requests/signatureSessionRequestWithNonce.json b/src/test/resources/requests/signatureSessionRequestWithNonce.json deleted file mode 100644 index 65681b7d..00000000 --- a/src/test/resources/requests/signatureSessionRequestWithNonce.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "nonce": "zstOt2umlc", - "allowedInteractionsOrder": [ - {"type": "confirmationMessage", "displayText200":"Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"}, - {"type": "displayTextAndPIN","displayText60": "Transfer 1 unit to account 7677323232?"} - ] -} diff --git a/src/test/resources/requests/signatureSessionRequestWithRequestProperties.json b/src/test/resources/requests/signatureSessionRequestWithRequestProperties.json deleted file mode 100644 index abe38396..00000000 --- a/src/test/resources/requests/signatureSessionRequestWithRequestProperties.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "requestProperties": { - } -} diff --git a/src/test/resources/requests/signatureSessionRequestWithSha512.json b/src/test/resources/requests/signatureSessionRequestWithSha512.json deleted file mode 100644 index 60becf99..00000000 --- a/src/test/resources/requests/signatureSessionRequestWithSha512.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "hhhE1nBOhXP+w02WfiC8/vPUJM9IvgTm3AjyvVjHKXQzcQFerYkcw88cnTS0kmS1EHUbH/nlN5N7xGtdb/TsyA==", - "hashType": "SHA512", - "allowedInteractionsOrder": [ - {"type": "confirmationMessage", "displayText200":"Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"}, - {"type": "displayTextAndPIN","displayText60": "Transfer 1 unit to account 7677323232?"} - ] -} diff --git a/src/test/resources/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json b/src/test/resources/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json deleted file mode 100644 index 77f19459..00000000 --- a/src/test/resources/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - { - "type": "confirmationMessage", - "displayText200": "Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?" - }, - { - "type": "verificationCodeChoice", - "displayText60": "Transfer 707 BSD to Oceanic Airlines?" - } - ] -} diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json b/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json deleted file mode 100644 index 86f09cc5..00000000 --- a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - { - "type": "confirmationMessage", - "displayText200": "Do you want to transfer 200 Bison dollars from savings account to Oceanic Airlines?" - }, - { - "type": "displayTextAndPIN", - "displayText60": "Transfer 200 BSD to Oceanic Airlines?" - } - ] -} diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json b/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json deleted file mode 100644 index 77f19459..00000000 --- a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - { - "type": "confirmationMessage", - "displayText200": "Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?" - }, - { - "type": "verificationCodeChoice", - "displayText60": "Transfer 707 BSD to Oceanic Airlines?" - } - ] -} diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_noFallback.json b/src/test/resources/requests/signingRequest_confirmationMessage_noFallback.json deleted file mode 100644 index fca8a3c3..00000000 --- a/src/test/resources/requests/signingRequest_confirmationMessage_noFallback.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - { - "type": "confirmationMessage", - "displayText200": "Do you want to transfer 999 Bison dollars from savings account to Oceanic Airlines?" - } - ] -} diff --git a/src/test/resources/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json b/src/test/resources/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json deleted file mode 100644 index 64cbbdc5..00000000 --- a/src/test/resources/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "relyingPartyUUID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "relyingPartyName": "BANK123", - "certificateLevel": "ADVANCED", - "hash": "0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg=", - "hashType": "SHA256", - "allowedInteractionsOrder": [ - { - "type": "verificationCodeChoice", - "displayText60": "Transfer 444 BSD to Oceanic Airlines?" - }, - { - "type": "displayTextAndPIN", - "displayText60": "Transfer 444 BSD to Oceanic Airlines?" - } - ] -} 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 new file mode 100644 index 00000000..79fb9fdb --- /dev/null +++ b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json @@ -0,0 +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" +} \ 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 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/authenticationSessionResponse.json b/src/test/resources/responses/authenticationSessionResponse.json deleted file mode 100644 index 324ae22c..00000000 --- a/src/test/resources/responses/authenticationSessionResponse.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "sessionID": "1dcc1600-29a6-4e95-a95c-d69b31febcfb", - "unknownField7233": "this field is added to verify that client doesn't fail when new fields are added" -} 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": "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", + "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": "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", + "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/certificateChoiceResponse.json b/src/test/resources/responses/certificateChoiceResponse.json deleted file mode 100644 index f90ed6a6..00000000 --- a/src/test/resources/responses/certificateChoiceResponse.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sessionID": "97f5058e-e308-4c83-ac14-7712b0eb9d86", - "unknownField935633": "this field is added to verify that client doesn't fail when new fields are added" - -} 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/session-status-document-unusable.json b/src/test/resources/responses/session-status-document-unusable.json new file mode 100644 index 00000000..82f93325 --- /dev/null +++ b/src/test/resources/responses/session-status-document-unusable.json @@ -0,0 +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" +} \ 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 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-running-with-ignored-properties.json b/src/test/resources/responses/session-status-running-with-ignored-properties.json new file mode 100644 index 00000000..b44c1615 --- /dev/null +++ b/src/test/resources/responses/session-status-running-with-ignored-properties.json @@ -0,0 +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" +} \ No newline at end of file diff --git a/src/test/resources/responses/sessionStatusRunning.json b/src/test/resources/responses/session-status-running.json similarity index 88% rename from src/test/resources/responses/sessionStatusRunning.json rename to src/test/resources/responses/session-status-running.json index e24a5213..221b89f7 100644 --- a/src/test/resources/responses/sessionStatusRunning.json +++ b/src/test/resources/responses/session-status-running.json @@ -1,5 +1,4 @@ { "state": "RUNNING", - "result": {}, "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 new file mode 100644 index 00000000..bdda60ba --- /dev/null +++ b/src/test/resources/responses/session-status-successful-authentication.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..df74f5e2 --- /dev/null +++ b/src/test/resources/responses/session-status-successful-certificate-choice.json @@ -0,0 +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" +} diff --git a/src/test/resources/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json new file mode 100644 index 00000000..f5372743 --- /dev/null +++ b/src/test/resources/responses/session-status-successful-signature.json @@ -0,0 +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": "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", + "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 new file mode 100644 index 00000000..32dae0d6 --- /dev/null +++ b/src/test/resources/responses/session-status-timeout.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..bd4616a7 --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused-cert-choice.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..62656ae6 --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..d3b1af6e --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused-confirmation.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..be9fecf6 --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..a06e5f09 --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused-vc-choice.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..7c3e55eb --- /dev/null +++ b/src/test/resources/responses/session-status-user-refused.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..9dd59fc9 --- /dev/null +++ b/src/test/resources/responses/session-status-wrong-vc.json @@ -0,0 +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" +} \ No newline at end of file diff --git a/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequest.json b/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequest.json deleted file mode 100644 index 2ff37aff..00000000 --- a/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-31111111111" - }, - "signature": { - "value": "luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv0SEk8Qk0yoM9ms/Qsq6OtFoTFZfmpZNQtmEDg6ADeVNUfZrAoozSbqGggNP9pZLv0pQ6fvkoUjV0XI3FP5PuICuRp0vfC2w024cG/Rw2XGfZTcDWZ3IWVaS/C9tqbvA7l0Ssa8W9wUit2H5msNMyyviHJFW7m9yfjdMZe9ebPw9HGSHjnRGbzl7myUIZyWtlD5mqnk/tcU13+lD6DBYkGSW5eu125W1BMojH3sIRrKhEdZxkgwtg6KEuWQ85OQBMF3X5zFB62caKqKo42HfV74EcbaBChhJ32226VZju+JKN9D+WUeQ==", - "algorithm": "sha256WithRSAEncryption" - }, - "cert": { - "value": "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==", - "certificateLevel": "QUALIFIED" - }, - "interactionFlowUsed":"displayTextAndPIN", - "unknownField1234586": "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/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json b/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json deleted file mode 100644 index 6d9777e5..00000000 --- a/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-31111111111" - }, - "signature": { - "value": "luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv0SEk8Qk0yoM9ms/Qsq6OtFoTFZfmpZNQtmEDg6ADeVNUfZrAoozSbqGggNP9pZLv0pQ6fvkoUjV0XI3FP5PuICuRp0vfC2w024cG/Rw2XGfZTcDWZ3IWVaS/C9tqbvA7l0Ssa8W9wUit2H5msNMyyviHJFW7m9yfjdMZe9ebPw9HGSHjnRGbzl7myUIZyWtlD5mqnk/tcU13+lD6DBYkGSW5eu125W1BMojH3sIRrKhEdZxkgwtg6KEuWQ85OQBMF3X5zFB62caKqKo42HfV74EcbaBChhJ32226VZju+JKN9D+WUeQ==", - "algorithm": "sha256WithRSAEncryption" - }, - "cert": { - "value": "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", - "certificateLevel": "QUALIFIED" - }, - "interactionFlowUsed":"displayTextAndPIN", - "deviceIpAddress": "62.65.42.45", - "unknownField1234586": "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/sessionStatusForSuccessfulCertificateRequest.json b/src/test/resources/responses/sessionStatusForSuccessfulCertificateRequest.json deleted file mode 100644 index 360d369d..00000000 --- a/src/test/resources/responses/sessionStatusForSuccessfulCertificateRequest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-31111111111" - }, - "cert": { - "value": "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", - "certificateLevel": "QUALIFIED" - }, - "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/sessionStatusForSuccessfulSigningRequest.json b/src/test/resources/responses/sessionStatusForSuccessfulSigningRequest.json deleted file mode 100644 index cb8931a3..00000000 --- a/src/test/resources/responses/sessionStatusForSuccessfulSigningRequest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-31111111111" - }, - "signature": { - "value": "luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv0SEk8Qk0yoM9ms/Qsq6OtFoTFZfmpZNQtmEDg6ADeVNUfZrAoozSbqGggNP9pZLv0pQ6fvkoUjV0XI3FP5PuICuRp0vfC2w024cG/Rw2XGfZTcDWZ3IWVaS/C9tqbvA7l0Ssa8W9wUit2H5msNMyyviHJFW7m9yfjdMZe9ebPw9HGSHjnRGbzl7myUIZyWtlD5mqnk/tcU13+lD6DBYkGSW5eu125W1BMojH3sIRrKhEdZxkgwtg6KEuWQ85OQBMF3X5zFB62caKqKo42HfV74EcbaBChhJ32226VZju+JKN9D+WUeQ==", - "algorithm": "sha256WithRSAEncryption" - }, - "interactionFlowUsed":"displayTextAndPIN", - "unknownField880624": "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/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json b/src/test/resources/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json deleted file mode 100644 index 04d14fd3..00000000 --- a/src/test/resources/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-31111111111" - }, - "signature": { - "value": "luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv0SEk8Qk0yoM9ms/Qsq6OtFoTFZfmpZNQtmEDg6ADeVNUfZrAoozSbqGggNP9pZLv0pQ6fvkoUjV0XI3FP5PuICuRp0vfC2w024cG/Rw2XGfZTcDWZ3IWVaS/C9tqbvA7l0Ssa8W9wUit2H5msNMyyviHJFW7m9yfjdMZe9ebPw9HGSHjnRGbzl7myUIZyWtlD5mqnk/tcU13+lD6DBYkGSW5eu125W1BMojH3sIRrKhEdZxkgwtg6KEuWQ85OQBMF3X5zFB62caKqKo42HfV74EcbaBChhJ32226VZju+JKN9D+WUeQ==", - "algorithm": "sha256WithRSAEncryption" - }, - "interactionFlowUsed":"displayTextAndPIN", - "deviceIpAddress": "62.65.42.46", - "unknownField880624": "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/sessionStatusRunningWithIgnoredProperties.json b/src/test/resources/responses/sessionStatusRunningWithIgnoredProperties.json deleted file mode 100644 index c344be7e..00000000 --- a/src/test/resources/responses/sessionStatusRunningWithIgnoredProperties.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": "RUNNING", - "result": {}, - "ignoredProperties":["testingIgnored","testingIgnoredTwo"], - "unknownField444211": "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/sessionStatusWhenDocumentUnusable.json b/src/test/resources/responses/sessionStatusWhenDocumentUnusable.json deleted file mode 100644 index b2b6fc31..00000000 --- a/src/test/resources/responses/sessionStatusWhenDocumentUnusable.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "DOCUMENT_UNUSABLE" - }, - "unknownField657859": "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/sessionStatusWhenRequiredInteractionNotSupportedByApp.json b/src/test/resources/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json deleted file mode 100644 index bc709dee..00000000 --- a/src/test/resources/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" - }, - "unknownField7899852": "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/sessionStatusWhenTimeout.json b/src/test/resources/responses/sessionStatusWhenTimeout.json deleted file mode 100644 index d05d4900..00000000 --- a/src/test/resources/responses/sessionStatusWhenTimeout.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "TIMEOUT" - }, - "unknownField73124": "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/sessionStatusWhenUnknownErrorCode.json b/src/test/resources/responses/sessionStatusWhenUnknownErrorCode.json deleted file mode 100644 index 0a002b5f..00000000 --- a/src/test/resources/responses/sessionStatusWhenUnknownErrorCode.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "UNKNOWN_ERROR_CODE" - }, - "unknownField462859": "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/sessionStatusWhenUserHasSelectedWrongVcCode.json b/src/test/resources/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json deleted file mode 100644 index 9a18d92d..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "WRONG_VC" - }, - "unknownField45622474": "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/sessionStatusWhenUserRefusedCertChoice.json b/src/test/resources/responses/sessionStatusWhenUserRefusedCertChoice.json deleted file mode 100644 index 93ca2256..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedCertChoice.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_CERT_CHOICE" - }, - "unknownField111122223": "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/sessionStatusWhenUserRefusedConfirmationMessage.json b/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessage.json deleted file mode 100644 index 4565bba7..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessage.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_CONFIRMATIONMESSAGE" - }, - "unknownField45332721": "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/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json b/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json deleted file mode 100644 index 6b9f47c1..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE" - }, - "unknownField44657321": "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/sessionStatusWhenUserRefusedDisplayTextAndPin.json b/src/test/resources/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json deleted file mode 100644 index 51db537e..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_DISPLAYTEXTANDPIN" - }, - "unknownField45622474": "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/sessionStatusWhenUserRefusedGeneral.json b/src/test/resources/responses/sessionStatusWhenUserRefusedGeneral.json deleted file mode 100644 index 480487e6..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedGeneral.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED" - }, - "unknownField498321": "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/sessionStatusWhenUserRefusedVerificationCodeChoice.json b/src/test/resources/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json deleted file mode 100644 index 303407fb..00000000 --- a/src/test/resources/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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" -} 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 new file mode 100644 index 00000000..ae3b7df6 --- /dev/null +++ b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json @@ -0,0 +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" +} \ 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 new file mode 100644 index 00000000..3e18ae14 --- /dev/null +++ b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json @@ -0,0 +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" +} \ 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 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 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 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 diff --git a/src/test/resources/responses/signatureSessionResponse.json b/src/test/resources/responses/signatureSessionResponse.json deleted file mode 100644 index 6dd51744..00000000 --- a/src/test/resources/responses/signatureSessionResponse.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "sessionID": "2c52caf4-13b0-41c4-bdc6-aa268403cc00", - "unknownField46932": "this field is added to verify that client doesn't fail when new fields are added in future" -} 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_trust_anchor_certificates.jks b/src/test/resources/sid_trust_anchor_certificates.jks new file mode 100644 index 00000000..309153e2 Binary files /dev/null and b/src/test/resources/sid_trust_anchor_certificates.jks differ 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/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/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/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----- 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/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/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/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----- 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/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/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 afa683a8..5074e4c4 100644 Binary files a/src/test/resources/trusted_certificates.jks and b/src/test/resources/trusted_certificates.jks differ 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_EID-SK_2016.pem.crt b/src/test/resources/trusted_certificates/TEST_of_EID-SK_2016.pem.crt deleted file mode 100644 index dec3f5cd..00000000 --- a/src/test/resources/trusted_certificates/TEST_of_EID-SK_2016.pem.crt +++ /dev/null @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIG+DCCBeCgAwIBAgIQUkCP5k8r59RXxWzfbx+GsjANBgkqhkiG9w0BAQwFADB9 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 -IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTYwODMwMTEyNDE1WhgP -MjAzMDEyMTcyMzU5NTlaMGgxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0 -aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEcMBoG -A1UEAwwTVEVTVCBvZiBFSUQtU0sgMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBAOrKOByrJqS1QsKD4tXhqkZafPMd5sfxem6iVbMAAHKpvOs4Ia2o -XdSvJ2FjrMl5szeT4lpHyzfECzO3nx7pvRLKHufi6lMwMGjtSI6DK8BiH9z7Lm+k -NLunNFdIir0hPijjbIkjg9iwfaeST9Fi5502LsK7duhKuCnH7O0uMrS/MynJ4StA -NGY13X2FvPW4qkrtbwsmhdN0Btro72O6/3O+0vbnq/yCWtcQrBGv3+8XEBdCqH5S -/Rt0EugKX4UlVy5l0QUc8IrjGtdMsr9KDtvmVwlefXYKoLqkC7guMGOUNf6Y4AYG -sPqfY4dG3N5YNp5FHDL7IO93h7TpRV3gyR38LiJsPHk5nES5mdPkNuEkCyg0zEKI -7uJ4LUuBbjzZPp2gP7PN8Iqi9GP7V2NCz8vUVN3WpHvctsf0DMvZdV5pxqLY5ojy -fhMsU4aMcGSQA9EK8ES3O1zBK1DW+btjbQjUFW1SIwCkB2yofFxge+vvzZGbvt2U -GOE8oAL8/JzNxi9FbjTAbycrGWgEMQ0sM1fKc+OsvoaSy9m3ZQGph0+dbsouQpl3 -kpJvjDMzxxkrMqxdhlVMreLKGCMMxJMAGQEwVS5P93Nnmz8UbkmeomUJr3NrBo4+ -V9L5S4Kx1vTvD0p72xRYFyfifLOjs8qs7lR3yhkcBPQI78ERqxv31FWDAgMBAAGj -ggKFMIICgTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNVHQ4E -FgQUrrDq4Tb4JqulzAtmVf46HQK/ErQwDgYDVR0PAQH/BAQDAgEGMIHEBgNVHSAE -gbwwgbkwPAYHBACL7EABAjAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5l -ZS9yZXBvc2l0b29yaXVtL0NQUzA8BgcEAIvsQAEAMDEwLwYIKwYBBQUHAgEWI2h0 -dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMDsGBgQAj3oBAjAxMC8G -CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS -BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD -AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v -b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0 -cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e -BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs -SQEBMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRv -cnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQAiw1VN -xp1Ho7FwcPlFqlLl6zb225IvpNelFX2QMbq1SPe41LuBW7WRZIV4b6bRQug55k8l -Am8eX3zEXL9I+4Bzai/IBlMSTYNpqAQGNVImQVwMa64uN8DWo8LNWSYNYYxQzO7s -TnqsqxLPWeKZRMkREI0RaVNoIPsciJvid9iBKTcGnMVkbrgyLzlXblLMU4I0pL2R -Wlfs2tr+XtCtWAvJPFskM2QZ2NnLjW8WroZr8TooocRA1vl/ruIAPC3FxW7zebKc -A2B66j4tW7uyF2kPx4WWA3xgR5QZnn4ePEAYjJdu1eWd9KbeAbxPCfFOST43t0fm -20HfV2Wp2PMEq4b2 ------END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_of_NQ-SK_2016.pem.crt b/src/test/resources/trusted_certificates/TEST_of_NQ-SK_2016.pem.crt deleted file mode 100644 index cdec952f..00000000 --- a/src/test/resources/trusted_certificates/TEST_of_NQ-SK_2016.pem.crt +++ /dev/null @@ -1,37 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGijCCBXKgAwIBAgIQOjiPZGsWs2VXxW0gWA+mAzANBgkqhkiG9w0BAQwFADB9 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 -IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTYwODMwMTEyNTIwWhgP -MjAzMDEyMTcyMzU5NTlaMGcxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0 -aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkG -A1UEAwwSVEVTVCBvZiBOUS1TSyAyMDE2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAwKLATeOt27z1OPLOFaUQVTLSL6tiQLrBZCO3C3DQuMLixR6cCla+ -aAS3U4VaKZRCrK+NI7v2cGvDdPW6jmztJJPlXcbZ2nY6QtQq2TkXnVx8Yh+9H1iR -B3u9Av9ALFEisj/uYWGoqA8bT7C0MgCu7VGdvpYpiRy7FCyKX7CDf3wW4a/x+vil -4yMb0UD2BTrMgwTgcxsaQ4zCg+DFvB8+97pOWZMWbBjkLskM/mxp/ChrDVRiQsMg -cUgiQ2heqRa3lNrHXkyJYseUEaCxXkT+aIwdtG7HPqvTrhLbfJs9iMFV3t08jFRZ -n8gwpUlyy0pztNoy6Xn6d9BHv5+P7/yIOMKghh23gx637WRIaghIn8+6i6/CIK77 -IQTxwwc4Prg/kpr+F7/5l7M/9Hk7yXsJZ5RHP+JooJcF25pU7VEO80UDJ/srKfm/ -frlHqeioUxmYRdZSRLiPiZpMC958euD5NsuiJSGqCtESGLyRxNp5Ts7iaQbMcRx0 -fHTJ0jG4EzXprUKCZCBD2ozK+DljyKEQZmwr7tXge9/JEiX1xhO4fGzadtz5nXjJ -vAnh8KUnTX9fli7Y1wY2Y2iBlYUbxn9ENPusE5TcLMKDnvpLEd7b0Z3keQiIWR0G -vNN59Fe2RhM4sa0IyNXyM0xvamglEEP9/uWJmEdYf7q0wBmWQUhcMc8CAwEAAaOC -AhgwggIUMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSxZUcbA3b+MB0GA1UdDgQW -BBSsw050xt/OPR3E74FhBbZv3UkdPTAOBgNVHQ8BAf8EBAMCAQYwRgYDVR0gBD8w -PTA7BgYEAI96AQEwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvcmVw -b3NpdG9vcml1bS9DUFMwEgYDVR0TAQH/BAgwBgEB/wIBADCBjQYIKwYBBQUHAQEE -gYAwfjAgBggrBgEFBQcwAYYUaHR0cDovL29jc3Auc2suZWUvQ0EwWgYIKwYBBQUH -MAKGTmh0dHBzOi8vd3d3LnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0Nl -cnRpZmljYXRpb25fQ2VudHJlX1Jvb3RfQ0EuZGVyLmNydDBBBgNVHR4EOjA4oTYw -BIICIiIwCocIAAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDAl -BggrBgEFBQcBAwQZMBcwFQYIKwYBBQUHCwIwCQYHBACL7EkBATBDBgNVHR8EPDA6 -MDigNqA0hjJodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9l -ZWNjcmNhLmNybDANBgkqhkiG9w0BAQwFAAOCAQEAt/0Rv4T7HcRt53ELEDvjTXdx -CmdAvLs/eynom18jTguHSwO1vqcq+FRjZ8Qw2Ds5lL8QqT9h38lrQoyNVXLSJOV4 -9seLM/So3k2I6agYXOtM1a+oQG6Si09dQVwMAk0y/7YOddVnx5OdGJZlJTqQumVJ -US96Wm74qKh6B+w0gGgAvg/7BpAIBtUweRmjoV4iT/EKz0bKOJPU63guw7y6APGJ -Osama9fj96cVrnqNdPhaPKqTIPkdabkwxB3wPiCzON9+r0FVUn0se4kIkqZ+jJQB -LmYCvnuzMwiYVBvWorTpWNXwLV7B8cwI5/UwmXermhgBhRhb4ZBQhuChRNEp4w== ------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----- diff --git a/src/test/resources/trusted_certificates/wrong.pem.cer b/src/test/resources/trusted_certificates/wrong.pem.cer deleted file mode 100644 index 23856b9c..00000000 --- a/src/test/resources/trusted_certificates/wrong.pem.cer +++ /dev/null @@ -1,37 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGezCCBWOgAwIBAgIQBs+E+B8gYnf1I31IIanXXjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E -aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkwMzIxMDAwMDAwWhcN -MjEwMzI1MTIwMDAwWjBQMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb -MBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRIwEAYDVQQDEwltaWQuc2suZWUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE0RI6DQ7wN5hKhlhCSN7Z -x68hIfGG54XktQLbnvSeJSHZqqSJTCYSkMPQ1cSTMolviHdOWl7qUzX7OCoseV+g -okvgig83amfPR25Qdt3vzvCLT0gj4GojKIYtSSRqU9lsXliib0lNypdBoPvUKicT -1WWHz8pnUv7ZK/iu9190hjGaUxbqmJWyFSjh8Olowr1I2mGCWf7ymAX5Lqnk5Gxi -J9r79e5JTPx0dOaIgC+Fo3ZrH1xSdpXb3ycSMWwMsYoLN1D4J8fIOBk4GDB1UwBJ -QMu3F90sXjbaJrwgHeHP6LNxKY3BYOe3uVy+zXiNcmIirr6x4oS0lL90QFSGq/R1 -AgMBAAGjggNSMIIDTjAfBgNVHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAd -BgNVHQ4EFgQU2+x3/zzTZeraNrpJb/B6SL1r4d4wFAYDVR0RBA0wC4IJbWlkLnNr -LmVlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH -AwIwawYDVR0fBGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3Nz -Y2Etc2hhMi1nNi5jcmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9z -c2NhLXNoYTItZzYuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYB -BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHwG -CCsGAQUFBwEBBHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu -Y29tMEYGCCsGAQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln -aUNlcnRTSEEyU2VjdXJlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF+Bgor -BgEEAdZ5AgQCBIIBbgSCAWoBaAB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDE -e4l6qP3LAAABaaDXZ0QAAAQDAEcwRQIgN7q4F8UJyQOT8OsG8h96BZHRdMUk4Aly -G7tztptFBW8CIQDF7tr5je9pxFzlczVwdq6LzlI9cnSnloCdgJ0E2/P5sQB2AId1 -v+dZfPiMQ5lfvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABaaDXaIIAAAQDAEcwRQIg -RSfaNfCLY/0tvCIw+oVusNddo4lSa++xCIqMvjnkZ6YCIQCv+UoMOs9kCd5yZbay -jXCbVuiNrWvDijYGGF2lfPWpDwB2AESUZS6w7s6vxEAH2Kj+KMDa5oK+2MsxtT/T -M5a1toGoAAABaaDXZwEAAAQDAEcwRQIgL3CaRptYqf/5EPebOO/QzWn9xJh2fbeu -BQaYCYNtECwCIQCBnj61xJxy361r1qAI5Y7EZIUWt8Z/9vxztACxf/mPMDANBgkq -hkiG9w0BAQsFAAOCAQEAPqjpkav+c7bZSMFRwTB3+t68UD0zG7JFRWblxqi4QcG8 -bTDoXfrZTp8nC0FQa56SbQVrFlkP6306+O9Itc09049J3qBZ3YDXNy4aetsL8LMa -VqF8mZadv2BQz6mCw56XLgKJVhKRA6QVHRgsocx9Ujp9NZsdP7JxhFIHXUAu6CHk -SYZoUeXL3/mwbr/ul6JvF5cQ8uyxVz7uw5narW9+I8hlzbAXLzL126MyAbQ+v45E -2goHz9848QEGlu6AtlCvcmp8VqO+BH6e4e4a+ihUaXy1ykCgCw4Nq+3VVARdVv6+ -s/OHdPfZDLVzkZJA4Vl/GqmJpFAUF+FtG/oFT5gmRw== ------END CERTIFICATE----- diff --git a/src/test/resources/wrong_ssl_cert.jks b/src/test/resources/wrong_ssl_cert.jks deleted file mode 100644 index 1f0ed615..00000000 Binary files a/src/test/resources/wrong_ssl_cert.jks and /dev/null differ diff --git a/travis.sh b/travis.sh deleted file mode 100644 index f0d0ba95..00000000 --- a/travis.sh +++ /dev/null @@ -1,20 +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" == "openjdk8" ]; 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 -else - ./mvnw test -fi