blerify / mdl
Blerify library to interface with endpoints to generate ISO-18013-5 compliant documents
Installs: 274
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 5
Forks: 0
Open Issues: 0
pkg:composer/blerify/mdl
Requires
- firebase/php-jwt: ^6.10
- phpseclib/phpseclib: ^3.0
- ramsey/uuid: ^4.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.68
This package is not auto-updated.
Last update: 2025-12-19 14:11:08 UTC
README
Description
Library to manage the interactions with Blerify API
Setup
- In your project ensure you have a folder named "config" which should contain your service account generated from Blerify Portal.
Examples
Create an mDL
<?php require 'vendor/autoload.php'; use Blerify\Authentication\JwtHandler; use Blerify\Licenses\MdlClient; use Blerify\Model\Request\AdditionalData; use Blerify\Model\Request\Assemble; use Blerify\Model\Request\Create; use Blerify\Model\Request\OrganizationUser; use Blerify\Model\Request\MdlData; use Blerify\Model\Request\NamespaceData; use Blerify\Model\Request\NamespaceEntry; use Blerify\Model\Request\OnHold; use Blerify\Model\Request\Options; use Blerify\Model\Request\Revoke; use Blerify\Model\Request\Sign; use Blerify\Model\Request\StateChangeMetadata; use Blerify\Model\Request\Validate; use Blerify\Model\Request\ValidityInfo; use Ramsey\Uuid\Uuid; // Input variables $baseEndpoint = 'https://api.staging.blerify.com'; $projectId = '7c37e269-56fa-4e38-85fc-707075fcf968'; $templateId = '1a58e41e-6f0b-4dff-a277-a2cd0ad9ea1a'; // Initialize JWT handler $jwtHandler = JwtHandler::new(__DIR__ . '/config/credentials.json'); // Initialize Mdl client $mdlClient = new MdlClient($baseEndpoint, $jwtHandler, $projectId); // Step 1: Create an unsigned mDL and signing message // echo "\n1. Create signing message: "; $portrait = 'FFD8FFE000104A46494600010101009000900000FFDB004300130D0E110E0C13110F11151413171D301F1D1A1A1D3A2A2C2330453D4947443D43414C566D5D4C51685241435F82606871757B7C7B4A5C869085778F6D787B76FFDB0043011415151D191D381F1F38764F434F7676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676FFC00011080018006403012200021101031101FFC4001B00000301000301000000000000000000000005060401020307FFC400321000010303030205020309000000000000010203040005110612211331141551617122410781A1163542527391B2C1F1FFC4001501010100000000000000000000000000000001FFC4001A110101010003010000000000000000000000014111213161FFDA000C03010002110311003F00A5BBDE22DA2329C7D692BC7D0D03F52CFB0FF75E7A7EF3E7709723A1D0DAE146DDFBB3C039CE07AD2BD47A7E32DBB8DD1D52D6EF4B284F64A480067DFB51F87FFB95FF00EB9FF14D215DE66AF089CE44B7DBDE9CB6890A2838EDDF18078F7ADD62D411EF4DB9B10A65D6B95A147381EA0D495B933275FE6BBA75C114104A8BA410413E983DFF004F5AF5D34B4B4CDE632D0BF1FD1592BDD91C6411F3934C2FA6AF6B54975D106DCF4A65AE56E856001EBC03C7CE29DD9EEF1EF10FC447DC9DA76AD2AEE93537A1BA7E4F70DD8EFF0057C6DFFB5E1A19854A83758E54528750946EC6704850CD037BCEB08B6D7D2CC76D3317FC7B5CC04FB6707269C5C6E0C5B60AE549242123B0E493F602A075559E359970D98DB89525456B51C951C8AFA13EA8E98E3C596836783D5C63F5A61A99FDB7290875DB4BE88AB384BBBBBFC7183FDEAA633E8951DB7DA396DC48524FB1A8BD611A5AA2A2432F30AB420A7A6D3240C718CF031FA9EF4C9AD550205AA02951DF4A1D6C8421B015B769DB8C9229837EA2BE8B1B0D39D0EBA9C51484EFDB8C0EFD8D258DAF3C449699F2EDBD4584E7AF9C64E3F96B9BEB28D4AC40931E6478C8E76A24A825449501D867D2B1DCDEBAE99B9C752AE4ECD6DDE4A179C1C1E460938F9149EF655E515C03919A289CB3DCA278FB7BF177F4FAA829DD8CE3F2AC9A7ECDE490971FAFD7DCE15EED9B71C018C64FA514514B24E8E4F8C5C9B75C1E82579DC1233DFEC08238F6ADD62D391ACC1C5256A79E706D52D431C7A0145140B9FD149EB3A60DC5E88CBBC2DA092411E9DC71F39A7766B447B344E847DCAC9DCB5ABBA8D145061D43A6FCF1E65CF15D0E90231D3DD9CFE62995C6DCC5CA12A2C904A15F71DD27D451453E09D1A21450961CBB3EA8A956433B781F1CE33DFED54F0E2B50A2B71D84ED6DB18028A28175F74FC6BDA105C529A791C25C4F3C7A11F71586268F4A66B726E33DE9EA6F1B52B181C760724E47B514520A5A28A283FFD9'; $signatureUsualMark = 'FFD8FFE000104A46494600010101009000900000FFDB004300130D0E110E0C13110F11151413171D301F1D1A1A1D3A2A2C2330453D4947443D43414C566D5D4C51685241435F82606871757B7C7B4A5C869085778F6D787B76FFDB0043011415151D191D381F1F38764F434F7676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676FFC00011080018006403012200021101031101FFC4001B00000301000301000000000000000000000005060401020307FFC400321000010303030205020309000000000000010203040005110612211331141551617122410781A1163542527391B2C1F1FFC4001501010100000000000000000000000000000001FFC4001A110101010003010000000000000000000000014111213161FFDA000C03010002110311003F00A5BBDE22DA2329C7D692BC7D0D03F52CFB0FF75E7A7EF3E7709723A1D0DAE146DDFBB3C039CE07AD2BD47A7E32DBB8DD1D52D6EF4B284F64A480067DFB51F87FFB95FF00EB9FF14D215DE66AF089CE44B7DBDE9CB6890A2838EDDF18078F7ADD62D411EF4DB9B10A65D6B95A147381EA0D495B933275FE6BBA75C114104A8BA410413E983DFF004F5AF5D34B4B4CDE632D0BF1FD1592BDD91C6411F3934C2FA6AF6B54975D106DCF4A65AE56E856001EBC03C7CE29DD9EEF1EF10FC447DC9DA76AD2AEE93537A1BA7E4F70DD8EFF0057C6DFFB5E1A19854A83758E54528750946EC6704850CD037BCEB08B6D7D2CC76D3317FC7B5CC04FB6707269C5C6E0C5B60AE549242123B0E493F602A075559E359970D98DB89525456B51C951C8AFA13EA8E98E3C596836783D5C63F5A61A99FDB7290875DB4BE88AB384BBBBBFC7183FDEAA633E8951DB7DA396DC48524FB1A8BD611A5AA2A2432F30AB420A7A6D3240C718CF031FA9EF4C9AD550205AA02951DF4A1D6C8421B015B769DB8C9229837EA2BE8B1B0D39D0EBA9C51484EFDB8C0EFD8D258DAF3C449699F2EDBD4584E7AF9C64E3F96B9BEB28D4AC40931E6478C8E76A24A825449501D867D2B1DCDEBAE99B9C752AE4ECD6DDE4A179C1C1E460938F9149EF655E515C03919A289CB3DCA278FB7BF177F4FAA829DD8CE3F2AC9A7ECDE490971FAFD7DCE15EED9B71C018C64FA514514B24E8E4F8C5C9B75C1E82579DC1233DFEC08238F6ADD62D391ACC1C5256A79E706D52D431C7A0145140B9FD149EB3A60DC5E88CBBC2DA092411E9DC71F39A7766B447B344E847DCAC9DCB5ABBA8D145061D43A6FCF1E65CF15D0E90231D3DD9CFE62995C6DCC5CA12A2C904A15F71DD27D451453E09D1A21450961CBB3EA8A956433B781F1CE33DFED54F0E2B50A2B71D84ED6DB18028A28175F74FC6BDA105C529A791C25C4F3C7A11F71586268F4A66B726E33DE9EA6F1B52B181C760724E47B514520A5A28A283FFD9'; $mdlData = MdlData::new()->familyName('Maravi')->givenName('Carlos')->birthDate('1987-03-15') ->issueDate('2025-10-08')->expiryDate('2028-09-30')->issuingCountry('US')->issuingAuthority('Acme') ->documentNumber('8-203-1365')->portrait($portrait)->drivingPrivileges([ json_decode('{"vehicle_category_code": "C","issue_date": "2025-08-25","expiry_date": "2028-09-30", "codes": [{"code": "210"}]}')]) ->unDistinguishingSign('PA')->nationality("PA")->signatureUsualMark($signatureUsualMark); $validityInfo = ValidityInfo::new()->signed("2025-10-08T10:10:18Z")->validFrom("2025-10-08T19:20:25Z")->validUntil("2030-02-13T10:10:18Z"); $devicePublicKey = '{ "kty":"EC", "x":"iBh5ynojixm_D0wfjADpouGbp6b3Pq6SuFHU3htQhVk", "y":"oxS1OAORJ7XNUHNfVFGeM8E0RQVFxWA62fJj-sxW03c", "crv": "P-256" }'; $pemIssuerCertificate = "-----BEGIN CERTIFICATE-----\nMIIBZDCCAQkCFF29EODSyXmgikPlx4EKyCHZDZYfMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMRAwDgYDVQQKDAdCbGVyaWZ5MB4XDTI1MDgyMDIyMjg1M1oXDTI3MDgxMDIyMjg1M1owNDELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB0JsZXJpZnkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATLIH4nNDW0yg/+4a7wMw5kUJCXHSPJW6LdnY4u1v3NNeVf/cbOjeJm6tUHSb8Ha4XVgvJjeKE0XD+jrwuh3sN7MAoGCCqGSM49BAMCA0kAMEYCIQDMzXYMzerk9BvgwmbVtJV0wAHaZ/8zsiI+N6oqeY3VRgIhAOS4mJCctkjPrcGxnj0yV/ovB8gtbQGGhnGOHauDYc+T\n-----END CERTIFICATE-----"; $namespaceData = NamespaceData::new() ->bloodType("O+") ->organDonor(false) ->citizenIdNumber("8-203-136512") ->citizenIdType("CEDULA"); $namespaceEntry = NamespaceEntry::new() ->title("org.iso.18013.5.1.PA") ->data($namespaceData); $additionalData = AdditionalData::new() ->mdlData($mdlData) ->validityInfo($validityInfo) ->devicePublicKey(json_decode($devicePublicKey)) ->certificate($pemIssuerCertificate) ->kid('gpWQnAjvAdLWCqQAFNglAVHlqVajGmZTPQ'); // ideally RFC-7638 // ->namespaces([$namespaceEntry]); $organizationUser = OrganizationUser::new()->id('8-203-1365')->did('did:lac1:1iT5g9gduT4Q5DWE2bnncfnBCnM9uXPWMrCTvhPf2a8wpHWJgFBEZn295t1h9ucnQyvJ'); // $options = Options::new()->onboard(true); // use it when the user (identified by citizenID) had a license // Note: When user has already a license use $options = Options::new()->update(true); $createRequest = Create::new()->templateId($templateId) ->additionalData($additionalData) ->organizationUser($organizationUser) ->options($options); $correlationId = Uuid::uuid4()->toString(); // echo "\ncorrelationId: " . $correlationId . "\n"; $createResponse = $mdlClient->create($createRequest, $correlationId); handleError($createResponse); echo "\n sha256 hash from base64url signing message from: 0x" . hash('sha256', base64_decode($createResponse->getSigningMessage()), false) . "\n"; echo "Ok\n"; // Step 2: Call to sign (ONLY FOR TESTING, do not use for production) echo "\n2. Calling API to sign Message: "; $signingMessage = $createResponse->getSigningMessage(); $issuerSigningJwk = '{"alg":"ES256","crv":"P-256","d":"ldzAeZg7nIo_KhmaPEJk8QBqUofxXfSIDNtfdeg4bd8","kid":"gpWQnAjvAdLWCqQAFNglAVHlqVajGmZTPQ","kty":"EC","use":"sig","x":"yyB-JzQ1tMoP_uGu8DMOZFCQlx0jyVui3Z2OLtb9zTU","y":"5V_9xs6N4mbq1QdJvwdrhdWC8mN4oTRcP6OvC6Hew3s"}'; // $issuerSigningJwk = '{"alg":"ES256","crv":"P-256","d":"HYc4DoW5InaxhASW1ePdQzQS_PAoiovAT6Jon4Ah3ro","kid":"gpWQnAjvAdLWCqQAFNglAVHlqVajGmZTPQ","kty":"EC","use":"sig","x":"rOJ3RbuAzjuysWrZ5Qpc7R5GU85dlkhtxjppC2vSqdc","y":"67uuT8zI3GJBDs_ieersfRV6c0SifmuihNAzYvKV_cA"}'; $signingRequest = Sign::new()->signingMessage($signingMessage)->jwk($issuerSigningJwk); $signResponse = $mdlClient->signTest($signingRequest, $correlationId); handleError($signResponse); echo "Ok\n"; // echo "\nSign Response: " . $signResponse->getSignature() . "\n" ; // Step 3: Assemble response echo "\n3. Assemble final mDL: "; $assembleRequest = Assemble::new() ->templateId($templateId)->signature($signResponse->getSignature()) // signature in hex ->kid("gpWQnAjvAdLWCqQAFNglAVHlqVajGmZTPQ")->certificate($pemIssuerCertificate) ->signatureType(Assemble::PLAIN_SIGNATURE_TYPE); $assembleResponse = $mdlClient->assemble($assembleRequest, $createResponse->getCredential()->getId(), $correlationId); handleError($assembleResponse); echo "Ok\n"; echo "mDL " . $assembleResponse . "\n"; // Step 4: OnHold mDL echo "\n4. OnHold mDL: "; $status = false; // if true it means to re-enable a suspended license $metadata = StateChangeMetadata::new()->code("xxx")->description("xxxxx")->category("xxxxx"); $onHoldRequest = OnHold::new()->status(true)->metadata($metadata); $onHoldResponse = $mdlClient->hold($onHoldRequest, $createResponse->getCredential()->getId(), $correlationId); handleError($onHoldResponse); echo "Ok\n"; echo "on-hold response: " . json_encode($onHoldResponse) . "\n"; // Step 5: Revoke mDL echo "\n5. Revoke mDL: "; $metadata = StateChangeMetadata::new()->code("yyyy")->description("yyyyy")->category("yyyyy"); $revokeRequest = Revoke::new()->metadata($metadata); $revokeResponse = $mdlClient->revoke($revokeRequest, $createResponse->getCredential()->getId(), $correlationId); handleError($revokeResponse); echo "Ok\n"; echo "revocation response: " . json_encode($revokeResponse) . "\n"; // irreversible state // Step 6: Validate mDL: echo "\n6, Validate mDL: "; $mdocToValidate = Validate::new()->mdoc($assembleResponse); $validationResponse = $mdlClient->validate($mdocToValidate, $createResponse->getCredential()->getId(), $correlationId); handleError($validationResponse); echo "Ok\n"; echo "validation response: " . json_encode($validationResponse) . "\n"; function handleError($response) { if (is_array($response) && !empty($response['error'])) { // Handle the error echo "Error occurred: " . $response['message'] . " (Code: " . $response['code'] . ")\n"; echo "Error details: " . json_encode($response['details']); exit; } }
DER signatures support
// Step 3: Assemble response echo "\n3. Assemble final mDL: "; $derSignature = '5439d3889838f26447dd9be258a415a18c4a5611368f23a3b58305516f9cc7a6337b3c26472a28550b5080fc2ca8fd1fa8fccd9b76ed9efe1d8c4c6c68f475e0';//$signResponse->getSignature(); $signature = Utils::derToPlainSignature($derSignature); if (is_array($signature) && !empty($signature['error'])) { // show trace echo "Error converting DER to plain: " . $signature['message'] . " (Code: " . $signature['code'] . ")\n"; print_r(["Trace: " => $signature['trace']]); exit; } $assembleRequest = Assemble::new() ->templateId($templateId)->signature($signature) // signature in hex ->kid("gpWQnAjvAdLWCqQAFNglAVHlqVajGmZTPQ")->certificate($pemIssuerCertificate) ->signatureType(Assemble::PLAIN_SIGNATURE_TYPE);