vatsake / smart-id-v3
PHP Client Library for Smart ID v3 API
Requires
- php: ^8.1
- composer: >=2.0
- php-http/discovery: ^1.20.0
- phpseclib/phpseclib: ~3.0
- psr/cache: ^2.0 || ^3.0
- psr/http-client-implementation: ^1.0.1
- psr/http-factory-implementation: *
- psr/log: ^3.0
Requires (Dev)
- cache/filesystem-adapter: ^1.2
- guzzlehttp/guzzle: ^7.10
- monolog/monolog: ^3.10
- phpunit/phpunit: ^10.0
- squizlabs/php_codesniffer: ^4.0
README
Warning
Development Status
This library is currently under development and has not yet reached a stable release.
A PHP library for interacting with the Smart ID v3 API with support for authentication, digital signatures, and certificate management.
Table of Contents
- Installation
- Requirements
- Quick Start
- Configuration
- Usage Flows
- Response Validation
- Error Handling
- Best Practices
- Troubleshooting
- FAQ
- Resources
- Testing
- Todo
Quick Start
use Cache\Adapter\Filesystem\FilesystemCachePool; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; use Monolog\Handler\StreamHandler; use Monolog\Logger; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\SmartIdEnv; use Vatsake\SmartIdV3\SmartId; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\Requests\NotificationAuthRequest; $filesystemAdapter = new Local(__DIR__); $filesystem = new Filesystem($filesystemAdapter); $pool = new FilesystemCachePool($filesystem); $log = new Logger('test'); $log->pushHandler(new StreamHandler(__DIR__ . '/log.log')); // 1. Initialize client $config = new SmartIdConfig( relyingPartyName: 'DEMO', relyingPartyUUID: '00000000-0000-4000-8000-000000000000', env: SmartIdEnv::DEMO, certificatePath: __DIR__ . '/certificates', cache: $pool, logger: $log // Optional ); $smartId = new SmartId($config); // 2. Create authentication request $rpChallenge = RpChallenge::generate(); $request = NotificationAuthRequest::builder()->withInteractions('Hello world') ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->build(); // 3. Start notification flow authentication $session = $smartId->notification() ->authentication() ->startDocument($request, 'PNOEE-40504040001-DEM0-Q'); // 4. Poll for session completion $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms ->getAuthSession(30000); // 30000ms timeout // 5. Validate response $response->validate() ->withCertificateValidation() ->withRevocationValidation() ->withSignatureValidation() ->check(); echo "Authentication successful!";
Installation
Install via Composer:
composer require vatsake/smart-id-v3
Note
This library uses php-http/discovery to automatically detect and use PSR-18 HTTP clients. You'll need to install a compatible HTTP client implementation:
composer require guzzlehttp/guzzle
Other compatible clients: symfony/http-client, curl-client, etc.
Note
This library requires a PSR-6 cache implementation. You must install a compatible cache adapter:
composer require cache/filesystem-adapter
Other compatible clients: cache/redis-adapter, cache/memcached-adapter, etc.
Requirements
- PHP ^8.1
- PSR-18 HTTP client implementation
- PSR-6 cache implementation
Configuration
Certificate Setup
The library requires intermediate and authority certificates to validate certificate chains.
Certificate format/location:
- All certificates must be in PEM format (text-based, not binary DER)
- Place all trusted certificates in a single directory
- Use an absolute path like
/path/to/certificateswhen configuringcertificatePath
Smart ID Certificates: Certificates are available to download at Smart ID documentation.
Client Initialization
Initialize the Smart ID client with SmartIdConfig:
$config = new SmartIdConfig( relyingPartyName: 'DEMO', relyingPartyUUID: '00000000-0000-4000-8000-000000000000', env: SmartIdEnv::DEMO, certificatePath: '/path/to/trusted-certificates', cache: $cache // \Psr\Cache\CacheItemPoolInterface );
Note
Cache is only used for CRL (Certificate Revocation List) caching to optimize revocation validation performance.
With HTTP Client and Logging:
For production deployments, configure a custom HTTP client and enable logging:
$client = new \GuzzleHttp\Client(); $log = new Monolog\Logger('smart-id'); $log->pushHandler(new StreamHandler(__DIR__ . '/log.log')); $filesystemAdapter = new Local(__DIR__); $filesystem = new Filesystem($filesystemAdapter); $pool = new FilesystemCachePool($filesystem); $config = new SmartIdConfig( relyingPartyName: 'DEMO', relyingPartyUUID: '00000000-0000-4000-8000-000000000000', env: SmartIdEnv::DEMO, certificatePath: '/path/to/certificates', httpClient: $client, logger: $log, cache: $pool );
SSL/HTTPS Configuration
Smart ID requires enhanced security measures including HTTPS key pinning. Configure your HTTP client to use public key pinning:
Option 1: Strict Key Pinning (Recommended for Production)
Pin directly to the Smart ID server's public key:
$client = new \GuzzleHttp\Client([ 'curl' => [ CURLOPT_PINNEDPUBLICKEY => 'sha256//....', ], ]);
Option 2: Intermediate Pinning
Pin to Smart ID's intermediate certificate instead of the leaf certificate. This reduces the need to update your configuration annually:
$client = new \GuzzleHttp\Client([ 'verify' => '/path/to/cert/bundle', // Contains only Smart ID's intermediate certificate 'curl' => [ CURLOPT_CAPATH => '/path/that/does/not/exist', // Disable system truststore ], ]);
Note: While intermediate pinning is more convenient, strict key pinning provides stronger security guarantees.
Usage Flows
Choosing the Right Flow
Smart ID supports multiple authentication and signature flows depending on your use case:
| Flow | User Initiation | Use Case |
|---|---|---|
| Device Link | User scans QR or opens app | Mobile-first, modern applications |
| Notification | Server initiates | Traditional web applications; less secure |
Device Link Flows
Device Link Flows allow users to initiate sessions themselves by scanning a QR code or pressing a button on a website/app that opens the Smart ID application directly.
Device Link Advantages:
Device Link - Anonymous authentication
Authenticate a user without requiring them to provide identity information upfront.
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Requests\DeviceLinkAuthRequest; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $rpChallenge = RpChallenge::generate(); $request = DeviceLinkAuthRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->deviceLink()->authentication()->startAnonymous($request); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getAuthSession(10000); // Maximum time in milliseconds to wait for session completion, you might need to specify your httpclient's timeout accordingly // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR);
Device Link - Identified authentication - National ID
Authenticate a specific user by their national identification number. Use when you know who the user is beforehand.
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Requests\DeviceLinkAuthRequest; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Identity\SemanticsIdentifier; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Enums\NaturalIdentityType; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $identifier = SemanticsIdentifier::builder() ->withType(NaturalIdentityType::NATIONAL_PERSONAL_NUMBER) ->withCountryCode('EE') ->withIdentifier('40404040009') ->build(); $rpChallenge = RpChallenge::generate(); $request = DeviceLinkAuthRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->deviceLink()->authentication()->startEtsi($request, $identifier); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getAuthSession(10000); // Maximum time in milliseconds to wait for session completion // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR);
Device Link - Identified authentication - Document Number
Authenticate a user using their Smart ID document number. Use when you know user's document number.
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Requests\DeviceLinkAuthRequest; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $rpChallenge = RpChallenge::generate(); $request = DeviceLinkAuthRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->deviceLink()->authentication()->startDocument($request, 'PNOEE-40404040009-MOCK-Q'); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getAuthSession(10000); // Maximum time in milliseconds to wait for session completion // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR);
Device Link - Anonymous signature with certificate Selection (LINKED session)
Allow user to choose which certificate to use for signing:
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Requests\DeviceLinkCertChoiceRequest; use Vatsake\SmartIdV3\Requests\LinkedRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); // Step 1: Certificate selection $request = DeviceLinkCertChoiceRequest::builder() ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->build(); $session = $smartId->deviceLink()->signing()->startAnonymousCertChoice($request); $_SESSION['session'] = $session; // Save it for later // Get certificate selection result $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getCertChoiceSession(10000); // Maximum time in milliseconds to wait for session completion $_SESSION['documentNo'] = $response->documentNumber; // Save it for later // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR); // Step 2: Sign with selected certificate (after successful cert-choice) $dataToSign = "hello world"; $request = LinkedRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withLinkedSessionId($session->getSessionId()) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->build(); $documentNo = $_SESSION['documentNo']; $smartId->notification()->signing()->startLinkedSigning($request, $documentNo); // Get session status $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion
Device Link - Identified signature - National ID
Sign documents for a specific user identified by national ID:
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Enums\NaturalIdentityType; use Vatsake\SmartIdV3\Identity\SemanticsIdentifier; use Vatsake\SmartIdV3\Requests\DeviceLinkSigningRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $identifier = SemanticsIdentifier::builder() ->withType(NaturalIdentityType::NATIONAL_PERSONAL_NUMBER) ->withCountryCode('EE') ->withIdentifier('40404040009') ->build(); $dataToSign = "hello world"; $request = DeviceLinkSigningRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->build(); $session = $smartId->deviceLink()->signing()->startEtsi($request, $identifier); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR);
Device Link - Identified signature - Document Number
Sign documents for a user identified by document number:
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Enums\DeviceLinkType; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Requests\DeviceLinkSigningRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $dataToSign = "hello world"; $request = DeviceLinkSigningRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->withInitialCallbackUrl('https://localhost/callback') // Mandatory for Web2App/App2App flows ->build(); $session = $smartId->deviceLink()->signing()->startDocument($request, 'PNOEE-40404040009-MOCK-Q'); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion // During status polling, refresh QR-code every second $session->getDeviceLink(DeviceLinkType::QR);
Notification Flows
Notification Flows are initiated by the server and push notifications to the user's mobile device. This is the traditional Smart ID flow.
Notification Advantages:
- Server-initiated, predictable flow
- No QR code generation or refresh needed
Notification - Authentication - National ID
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Requests\NotificationAuthRequest; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Identity\SemanticsIdentifier; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Enums\NaturalIdentityType; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $identifier = SemanticsIdentifier::builder() ->withType(NaturalIdentityType::NATIONAL_PERSONAL_NUMBER) ->withCountryCode('EE') ->withIdentifier('40504040001') ->build(); $rpChallenge = RpChallenge::generate(); $request = NotificationAuthRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->notification()->authentication()->startEtsi($request, $identifier); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getAuthSession(10000); // Maximum time in milliseconds to wait for session completion
Notification - Authentication - Document Number
Authenticate a user using their Smart ID document number (server-initiated):
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Requests\NotificationAuthRequest; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Utils\RpChallenge; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $rpChallenge = RpChallenge::generate(); $request = NotificationAuthRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withRpChallenge($rpChallenge, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->notification()->authentication()->startDocument($request, 'PNOEE-40504040001-DEM0-Q'); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getAuthSession(10000); // Maximum time in milliseconds to wait for session completion
Notification - Signature - National ID
Sign documents for a user identified by national ID (server-initiated):
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Enums\NaturalIdentityType; use Vatsake\SmartIdV3\Identity\SemanticsIdentifier; use Vatsake\SmartIdV3\Requests\NotificationSigningRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $identifier = SemanticsIdentifier::builder() ->withType(NaturalIdentityType::NATIONAL_PERSONAL_NUMBER) ->withCountryCode('EE') ->withIdentifier('40504040001') ->build(); $dataToSign = "hello world"; $request = NotificationSigningRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->notification()->signing()->startEtsi($request, $identifier); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion
Notification - Signature - Document Number
Sign documents for a user identified by document number (server-initiated):
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Requests\NotificationSigningRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $dataToSign = "hello world"; $request = NotificationSigningRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->notification()->signing()->startDocument($request, 'PNOEE-40504040001-DEM0-Q'); $_SESSION['session'] = $session; // Save it for later // Get session status $ses = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion
Notification - Signature with Certificate Selection - National ID
Allow a user to select their certificate before signing. First perform a certificate choice, then sign with the selected certificate:
use Vatsake\SmartIdV3\Enums\CertificateLevel; use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\Enums\HashAlgorithm; use Vatsake\SmartIdV3\Enums\NaturalIdentityType; use Vatsake\SmartIdV3\Identity\SemanticsIdentifier; use Vatsake\SmartIdV3\Requests\NotificationCertChoiceRequest; use Vatsake\SmartIdV3\Requests\NotificationSigningRequest; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); $identifier = SemanticsIdentifier::builder() ->withType(NaturalIdentityType::NATIONAL_PERSONAL_NUMBER) ->withCountryCode('EE') ->withIdentifier('40504040001') ->build(); // Step 1: Let user choose certificate $request = NotificationCertChoiceRequest::builder() ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $session = $smartId->notification()->signing()->startCertChoice($request, $identifier); $_SESSION['session'] = $session; // Save it for later // Get certificate selection result $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getCertChoiceSession(10000); // Maximum time in milliseconds to wait for session completion $_SESSION['documentNo'] = $response->documentNumber; // Save it for later // Step 2: Sign with selected certificate (after successful cert-choice) $dataToSign = "hello world"; $request = NotificationSigningRequest::builder() ->withInteractions( 'display text up to 60 characters', 'display text up to 200 characters' ) ->withData($dataToSign, HashAlgorithm::SHA_256) ->withCertificateLevel(CertificateLevel::QUALIFIED) // Optional ->build(); $documentNo = $_SESSION['documentNo']; $session = $smartId->notification()->signing()->startDocument($request, $documentNo); // Get session status $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion
Certificate Retrieval
Retrieve a user's signing certificate without initiating a full authentication or signing session:
use Vatsake\SmartIdV3\Config\SmartIdConfig; use Vatsake\SmartIdV3\SmartId; $config = new SmartIdConfig(...); $smartId = new SmartId($config); // Retrieve certificate for a document $smartId->getSigningCertificate('PNOEE-40504040001-DEM0-Q');
Response Validation
After receiving a response from Smart ID, you should always validate the response to ensure the signature is valid, the certificate is trusted, and the certificate hasn't been revoked. The library provides a fluent validation API:
use Vatsake\SmartIdV3\Exceptions\Validation\ValidationException; // Client signed something /** @var Vatsake\SmartIdV3\Session\SigningSession */ $response = $smartId->session($session) ->withPolling(1000) // Poll every 1000ms (optional, will repeat polling automatically) ->getSigningSession(10000); // Maximum time in milliseconds to wait for session completion try { $response->validate() ->withSignatureValidation(true) // Validates the signature (enabled by default) ->withCertificateValidation(true) // Validates certificate chain ->withRevocationValidation(true) // Checks if certificate is revoked (OCSP or CRL) ->withCallbackUrlValidationParameters(...) // Validate callback url in Web2App/App2App flows ->check(); // Response is valid, proceed with your logic echo "Signature is valid!"; } catch (IncompleteSessionException $e) { // Session not complete yet } catch (UserRefusedException $e) { // User refused the request } catch (SessionTimeoutException $e) { // Session timed out } catch (DocumentUnusableException $e) { // Document unusable - check Smart-ID app for details } catch (WrongVcException $e) { // User entered wrong verification code } catch (RequiredInteractionNotSupportedByAppException $e) { // User's app doesn't support required interaction } catch (UserRefusedCertChoiceException $e) { // User refused certificate choice } catch (UserRefusedInteractionException $e) { // User cancelled on interaction screen } catch (ProtocolFailureException $e) { // Logical error in signing protocol } catch (ExpectedLinkedSessionException $e) { // App received different transaction instead of linked session } catch (ServerErrorException $e) { // Smart-ID server error } catch (ValidationException $e) { // - SignatureException if signature validation fails // - CertificateChainException if certificate chain is invalid // - OcspCertificateRevocationException if certificate is revoked // - OcspResponseTimeException if OCSP response time is outside acceptable window // - OcspSignatureException if OCSP response signature is invalid // - OcspKeyUsageException if OCSP responder certificate key usage is invalid // - OcspUrlMissingException if OCSP URL is not found // - CrlRevocationException if certificate is revoked according to CRL or CRL request fails // - CrlSignatureException if CRL signature is invalid // - CrlUrlMissingException if CRL URL is not found // - UnknownSignatureAlgorithmOidException if signature algorithm OID is unknown // - CertificatePolicyException if required Smart-ID policy OIDs are missing // - CertificateKeyUsageException if key usage or extended key usage is invalid // - CertificateQcException if qualified certificate does not contain required QC statements // - SessionSecretMismatchException if session secret digest does not match expected value // - InitialCallbackUrlParamMismatchException if callback URL query parameter does not match expected value // - UserChallengeMismatchException if user challenge verifier does not match expected value } catch (Exception $e) { // Something unexpected went wrong }
Error Handling
Exception Hierarchy
Exceptions are organized in a hierarchy to help with error handling:
Session State Exceptions - User or session state issues:
| Exception | Description |
|---|---|
IncompleteSessionException |
The session is not complete yet. Try polling again. |
UserRefusedException |
User refused the request (e.g., rejected in Smart-ID app). |
SessionTimeoutException |
Session timed out. User didn't respond in time. |
DocumentUnusableException |
Request failed. Check Smart-ID app logs or contact support. |
User Interaction Exceptions - Issues with user interactions:
| Exception | Description |
|---|---|
WrongVcException |
User entered the wrong verification code. |
RequiredInteractionNotSupportedByAppException |
User's app version doesn't support required interactions. |
UserRefusedCertChoiceException |
User refused to choose a certificate. |
UserRefusedInteractionException |
User cancelled on the interaction screen. |
Protocol Exceptions - Protocol-level issues:
| Exception | Description |
|---|---|
ProtocolFailureException |
A logical error occurred in the signing protocol. |
ExpectedLinkedSessionException |
App received different transaction instead of linked session. |
ServerErrorException |
Smart-ID server returned an error. |
Validation Exceptions - Certificate and signature validation failures:
| Exception | Description |
|---|---|
ValidationException |
Generic validation failure (parent of others below). |
SignatureException |
Signature validation failed. |
OcspUrlMissingException |
OCSP responder URL is not found in certificate. |
CrlRevocationException |
Certificate is revoked according to CRL or CRL request failed. |
CrlSignatureException |
CRL signature validation failed. |
CrlUrlMissingException |
CRL URL is not found in certificate. |
UnknownSignatureAlgorithmOidException |
Signature algorithm OID is unknown or not supported. |
CertificateChainException |
Certificate chain validation failed. Not trusted. |
OcspCertificateRevocationException |
OCSP status indicates certificate is revoked. |
OcspKeyUsageException |
OCSP responder certificate missing OCSP signing key usage. |
OcspResponseTimeException |
OCSP response time outside acceptable clock skew window. |
OcspSignatureException |
OCSP response signature validation failed. |
CertificateKeyUsageException |
Certificate key usage validation failed. |
CertificatePolicyException |
Certificate missing required Smart-ID policy OIDs. |
CertificateQcException |
Certificate missing required QC statements. |
InitialCallbackUrlParamMismatchException |
Initial callback URL unique parameter mismatch. |
SessionSecretMismatchException |
Session secret mismatch. |
UserChallengeMismatchException |
User challenge mismatch. |
Best Practices
- Always validate responses - Never trust unvalidated signatures or certificates
- Enable all validations - Use
withCertificateValidation(true),withRevocationValidation(true),withCallbackUrlValidation(true) (App2App/Web2App flows), andwithSignatureValidation(true) - Log exceptions - Capture exception details for debugging and monitoring
- Provide user feedback - Inform users of temporary issues (timeouts, refusal) vs. permanent errors (revoked certificate)
Troubleshooting
Common Issues
Certificate validation failures
If you're getting CertificateChainException or other certificate errors:
- Verify all your trusted certificates are in PEM format
- Check that the path is correct
- Ensure certificate file permissions allow reading (644 or similar)
OCSP validation errors
If you encounter out-of-memory errors during revocation checks:
- Increase memory. Certificate Revocation Lists (CRLs) can be large files containing tens of thousands of revoked certificate entries. During parsing, these are loaded into memory, which consume significant resources.
If you're getting OcspCertificateRevocationException or other OCSP errors:
- Verify your system has correct time/date (OCSP responses include time validation)
- Check network connectivity to OCSP responders
- Enable debug logging
Errors when scanning QR code
- Ensure QR codes are refreshed every 1-2 seconds during polling
- Verify QR code is large enough to scan
- Check that
withInitialCallbackUrlis set correctly for Web2App/App2App flows - Confirm the callback URL is accessible from the user's device
Debug Logging
Enable detailed logging to troubleshoot issues:
use Monolog\Logger; use Monolog\Handler\StreamHandler; $logger = new Logger('smart-id'); $logger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG)); $config = new SmartIdConfig( ... logger: $logger );
FAQ
** How would I know if user is on mobile? **
Note: This is relevant for Web2App flows where you may want to adjust UX based on device type.
There really isn't a foolproof method. One way is using the User-Agent header, but it can be spoofed or changed by the user.
// Frontend code - JavaScript function isOnMobile() { const ua = navigator.userAgent return /mobi|iphone|ipod|ipad|android/i.test(ua) }
Resources
Official Documentation:
Testing
Run tests with PHPUnit:
composer test
Note
Integration tests make real API calls to Smart-ID (DEMO environment).
Todo
- Figure out why mocking in DEMO doesn't work
- Rewrite tests
- Test App2App and Web2App flows
- More stuff