Example Server for BLE Credential Exchange
Overview
This guide demonstrates how to run a reference backend implementing BLE credential exchange with Last Lock devices using the Link Mobile SDK. It explains how to configure locks and grant unlock passes to perform BLE unlocks. This example uses the public Link gRPC API to communicate with Link, and set up lock and issue unlock passes.
Note: Example code only — not for production use. This code demonstrates SDK integration and is provided “as is,” without warranties or guarantees of security, correctness, or support. It omits production safeguards and has not been validated for use in a production access-control system. You are responsible for implementing and independently validating authentication, authorization, key management, and other security controls before deploying any system based on this example
Reference backend
token_unlock_server.py is a small Python reference implementation for integrators using Link and Link Mobile SDK. The implementation of the file
is present in the /examples directory of the SDK repo.
It uses the public Link gRPC API protobuf stubs and public PyPI packages for quick and easy setup.
The next sections list out the steps to setup the example server perform requests with Link.
Generate the Python protobuf stubs
Clone the Link gRPC protobuf definitions and create a gen/ directory. This gen/
directory will contain the generated python stubs form the proto definitions:
git clone git@github.com:last-lock/seamless-link.git
mkdir -p gen
We have provided an example Python server token_unlock_server.py
containing a simple implementation for configuring locks for BLE unlocks and issuing unlock passes. The example server uses the generated python stubs in gen/
make sure to include python server file in the same directory as gen/.
curl -fsSL \
https://raw.githubusercontent.com/last-lock/link-mobile-sdk-prod/main/examples/ExampleServer/token_unlock_server.py \
-o token_unlock_server.py
Create and activate the python virtual environment, then install the pinned dependencies for the example server:
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
Generate the stubs from the proto files fetched from the public Link gRPC API:
PROTO_DIR="$(cd seamless-link/proto && pwd)"
GRPC_TOOLS_PROTO="$(python -c 'import grpc_tools; from pathlib import Path; print(Path(grpc_tools.__file__).resolve().parent / "_proto")')"
python -m grpc_tools.protoc \
-I"$PROTO_DIR" \
-I"$GRPC_TOOLS_PROTO" \
--python_out=gen \
--grpc_python_out=gen \
$(find "$PROTO_DIR" -type f -name '*.proto' -print | sort)
Confirm that gen/ contains the python protobuf bindings before starting the server. Ensure gen/ is present beside
token_unlock_server.py; the server adds that directory to its Python module search path. Do not manually edit generated files.
Architectural Overview
In this guide, the reader is the lock, the user device is the phone running Link Mobile SDK, and the Credential Issuer is the integrator's (you, the reader's) backend (the example Python server). Link contains the device's Aliro configuration which the integrating backend can fetch. This configuration is used by the integrating backend to configure the lock as well as when issuing unlock passes.
In order to perform a credential exchange with the lock, the lock has to be configured for BLE Aliro credential exchange. Configuring a lock requires preparing the lock to recognize unlock passes from the integrating server. All locks report their Aliro reader public key, see the Aliro 1.0 spec, to Link. During this configuration step, the integrating server signs that key to create a reader certificate, then sends Link the lock's reader-group identifier, reader certificate, trusted Credential Issuer public key, Reader CA public key, and Data Element Identifier. Link then queues this configuration for delivery to the lock via SDK connectivity. Once this configuration has been accepted by the lock, the lock can verify matching signed Access Documents presented by the phone's unlock pass during BLE unlock.
Note: Configuring a lock does not issue a pass or grant access. It sets up the lock to validate signed Access Documents presented by a phone during a BLE unlock.
To issue a BLE unlock credential, the phone app must call the fetchPublicCredential() function from the SDK
with a string value called namespace that the created credential is tied to. Once the credential has been created
by the SDK, the app receives the credential's P-256 uncompressed public key, which it sends to the integrating
server to request an unlock pass. The unlock pass contains a signed Access Document bound to that public key and
reader-group trust material that lets the SDK authenticate the lock during the BLE exchange. The credential's
private key remains on the phone.
What is an Access Document?
An Aliro Access Document is a signed statement from the Credential Issuer that tells the lock that a particular mobile credential may be considered for access. It tells the lock which mobile credential public key the issuer is vouching for, when the document is valid, and what access information the issuer has supplied. The lock verifies the issuer’s signature and checks that the phone holds the matching private key before using that information to make an access decision. For the format and verification rules, see the Aliro 1.0 specification, sections 7 and 8.4.
The Access Document created by this example server contains two related parts:
IssuerAuth contains a signed Mobile Security Object (MSO). It binds the
mobile credential's public key, a validity interval, and a digest of the
access data to the Credential Issuer's signature. The matching private key
remains in the SDK on the phone. A copy of the document alone cannot prove
possession of that private key.
An IssuerSignedItem carries the configured element identifier
(TOKEN_UNLOCK_ACCESS_ELEMENT_ID) and the example's access data. The element
identifier names an access level that the lock can request during the credential exchange.
An issuer can define an identifier for one lock or for a group of locks. A lock can be
configured to request multiple identifiers, and a phone can hold Access
Documents for different identifiers. The lock considers a document only when
it contains a signed element matching a requested identifier. The signed digest
prevents the phone from changing the element without invalidating the document.
This example issues one element with a version and an opaque access-data ID; it
does not encode a per-lock device ID or a schedule.
The server returns this document as access_document in the
/v1/issue-unlock-pass response. The response also contains reader_groups,
which tells the phone how to verify the locks in a reader group. Each entry
contains a group identifier and the Reader CA trust material for that group.
During BLE unlock, the lock shares its group identifier with the SDK. The
SDK matches it to an entry in the stored pass and uses that entry's Reader CA
to verify the lock's certificate. If no entry matches, the SDK rejects the unlock attempt.
This example uses one group for all configured locks, so the response has one
entry even when several locks are configured.
How the phone and lock establish trust
A lock must verify who granted the phone access, while the phone must verify that it is communicating with a trusted lock. The organization issuing access may differ from the one managing the locks, for example, an employer and a building operator. Aliro's trust framework, section 6.1 also allows one organization to perform both roles. This example server does both jobs, but uses a separate P-256 signing key for each:
- The Credential Issuer key signs Access Documents. The lock receives the corresponding public key during configuration so it can verify documents presented by phones.
- The Reader System CA key signs each lock's reader certificate. The phone receives the corresponding CA trust material with the pass so the SDK can authenticate the lock before presenting the credential.
The server creates and persists its 16-byte reader-group identifier. As
described in Aliro 1.0, section 6.2,
the phone uses this identifier to select an Access Credential and the reader
key or CA certificate needed to authenticate the lock.
/v1/configure places it and each lock's reader certificate in the lock's
Aliro configuration. The pass supplies the same identifier and Reader CA trust
material to the phone. The identifier selects a credential and trust material;
knowing it alone does not grant access.
Configuration and pass issuance are different operations. During
/v1/configure, the server obtains the lock's reader public key from Link,
signs its reader certificate, and queues the reader-group identifier, reader
certificate, Credential Issuer public key, Reader CA public key, and accepted
access element identifier for delivery to that lock. The lock must receive
this configuration before it can accept a pass from this issuer. During
/v1/issue-unlock-pass, the phone supplies its SDK-created credential public
key and a duration in seconds. The server signs an Access Document bound to
that public key and returns it with the phone-side reader trust material. It
does not send the document or the phone's private key to the lock through Link.
During an unlock, the SDK authenticates the lock's reader certificate against the pass's reader-group trust material and uses the credential private key in the BLE exchange. The lock checks that the phone proves possession of the public key bound into the Access Document, that the document's issuer signature and validity interval are acceptable, and that the requested access element matches its configuration. A pass is useful only when these checks and the lock's access policy succeed. The unlock exchange can then occur without a new pass request to the backend.
Selecting a lock or group of locks
The access element identifier connects the lock's configuration to the
issuer-signed pass. Under Aliro 1.0, section 7.3,
the issuer uses this name to identify an access level. The lock requests its
configured name during the exchange and checks that the Access Document
contains a valid, signed element with that name. In this example server,
/v1/configure puts ELEMENT_ID into access_document_element_ids, and
issue_access_document() signs an IssuerSignedItem with the same ELEMENT_ID.
| Intended access | Configure the locks to request | Include in the signed Access Document |
|---|---|---|
Only lock 123 | lock.123 on lock 123 | An element named lock.123 |
An installer group of locks 123 and 124 | group.installers on both locks | An element named group.installers |
These names are examples, not special Aliro values. A matching name is
necessary but is not a substitute for checking the caller's authorization.
The backend must decide which locks the caller may use and sign only the
corresponding element names. To implement either row, replace this sample's
single global ELEMENT_ID with a mapping of locks to accepted element names
for configuration, then select the authorized name when issuing the signed
document. The mobile app should not be allowed to choose an arbitrary name
without the backend checking its access rights.
As written, every lock configured by this server accepts the same issuer key
and element name. Merely changing TOKEN_UNLOCK_ACCESS_ELEMENT_ID to
lock.123 would rename the element for every configured lock, not limit it to
lock 123. The configured_devices list only records which locks had
configuration queued; it is not an access-control list, and the pass request
does not include a lock ID. Likewise, the reader-group identifier helps the
phone authenticate locks; it does not restrict which locks may accept the
Access Document.
Example: a seven-day installer pass
Suppose an installer needs access to lock 123 for one week. The steps below
show an unlock at that lock with the unmodified example server; they do not
limit the pass to lock 123. To produce a lock-specific pass instead, change
both configuration and issuance to use the lock.123 mapping above, then
follow this flow:
-
Choose the access element. Decide which locks should accept this pass and which element name they will request. The unmodified server uses its single
ELEMENT_IDfor every configured lock. For access only to lock123, configure that lock to requestlock.123and issue a document containinglock.123, as described above. -
Configure the lock. Call
POST /v1/configurewith{"device_id":123}. The integrating server obtains lock123's reader public key from Link, signs its reader certificate, and queues the reader-group identifier, reader certificate, Reader CA public key, Credential Issuer public key, and accepted access element name for delivery to the lock. A202 Acceptedresponse means the configuration was queued; connect the lock through Link Mobile SDK and allow it to receive the configuration before testing. -
Create the phone credential. The app calls the SDK's
fetchPublicCredential(namespace)with a namespace it will persist. The SDK creates a credential key pair and returns its public key. The private key of the credential always remains on the phone. -
Issue the one-week pass. The app calls
POST /v1/issue-unlock-passwith the Base64-encoded public key incredential_public_keyandduration: 604800. The server signs an Access Document containing the selected element name, public key, and validity period, then returnsaccess_documentandreader_groups. It does not send the document to the lock through Link. -
Store the pass on the phone. The app converts the response to an SDK
TokenUnlockPassand callsstoreTokenUnlockPass(namespace, pass). It retains the namespace so the SDK can retrieve the credential and pass after an app restart. -
Unlock over BLE. At lock
123, the app queues the SDK's token-unlock operation with that namespace. The SDK usesreader_groupsto authenticate the lock, then presents the signed document and proves possession of the credential private key. The lock requests an access element identifier from its configuration and checks for a matching, issuer-signed element in the document. It also checks the issuer signature, validity period, and its access policy before unlocking. The reader-group identifier authenticates the lock to the phone; the access element identifier determines which configured access level the document can satisfy.To adapt this flow, an integrator can configure lock
123to requestlock.123and issue that element only to people authorized for that lock. Alternatively, configure locks123and124to requestgroup.installersand issue that element to an installer authorized for both. This requires changing both the element identifiers sent during/v1/configureand those signed during/v1/issue-unlock-pass; changing only the app's unlock call does not change the pass's access scope. Use the SDK's CredLink callbacks to report success or failures to the app; queuing the operation alone does not mean the lock opened.
If another lock is configured with the same issuer key and accepted element name while this pass is valid, the lock may accept the pass, even if the device was configured after the pass was issued.
Scope of this example: This server illustrates lock trust configuration
and pass issuance. Every configured lock receives the same Credential Issuer key and access element
identifier, so a pass may be accepted by any lock with that configuration.
When the sample app calls the issue-unlock-pass endpoint, the server signs a pass for the
SDK supplied credential public key and duration, up to seven days. A production
implementation must authenticate configuration and issuance requests, define
which element identifiers each lock accepts, and authorize the requested
access scope and duration before signing. Preserve the signing keys and
reader-group state used to configure locks, and limit this example server to
a trusted development network.
The reference server exposes these endpoints:
| Endpoint | Purpose |
|---|---|
GET /healthz | Liveness check; no authentication required. |
POST /v1/configure | Fetches a lock's reported reader public key from Link, issues a reader certificate, and queues its Aliro configuration in Link. |
POST /v1/issue-unlock-pass | Issues a pass for an SDK-created credential public key with the requested duration in seconds (up to seven days). |
Build and run
Use Python 3.9.2 or later and complete Generate the Python protobuf stubs before starting the server. Use the same directory and activated virtual environment for the following steps; you do not need to reinstall the dependencies or regenerate the stubs.
-
Set the Link connection and server configuration.
ORG_NAMEis your Link organization slug (for example,example-org), not a URL. The example server connects to<org>.link.lastlock.com:443using TLS.export LINK_API_KEY='<link-api-key>'export ORG_NAME='example-org'# Optional: defaults to port 3006 and the element ID sample.token-unlock.export TOKEN_UNLOCK_PORT=3006export TOKEN_UNLOCK_ACCESS_ELEMENT_ID='sample.ble-unlock'# By default, the server persists keys under ./token-unlock-dir, modify if another path is requiredexport TOKEN_UNLOCK_DATA_DIR="$PWD/token-unlock-dir"token-unlock-dirfolder stores the signing keys and reader-group state. Preserve this directory across restarts. If its keys are lost, configured locks will not trust newly issued passes until they are configured again. Changing the access element ID also requires reconfiguring the locks. Reconfigure existing locks if you change the Reader CA certificate format. -
Start the server from the directory containing
token_unlock_server.pyandgen/:python3 token_unlock_server.py -
Verify that the server is running:
curl -i http://localhost:3006/healthzThe response should be
200 OKwith{"status":"ok"}. -
Configure a lock for BLE credential exchange:
curl -i -X POST http://localhost:3006/v1/configure \-H 'Content-Type: application/json' \--data '{"device_id":123}'A
202 Acceptedresponse means Link has queued the configuration, not that the lock has applied it. Connect to the lock through Link Mobile SDK and allow the queued configuration to reach the lock before testing an unlock.
The server is then ready to issue passes for SDK-created credential public keys. The examples below show both pass durations; the iOS and Android guides show how to store a pass and use it during a BLE unlock. Keep this unauthenticated HTTP server on a trusted development network. Before exposing an equivalent production service, add TLS, caller authentication, and per-lock authorization.
Issuing an Aliro BLE credential
Before issuing a pass, configure each target lock through /v1/configure by
sending its device_id. Then call /v1/issue-unlock-pass with the credential's
Base64-encoded credential_public_key and the desired duration in seconds.
The example server issues passes for locks configured with its trust material.
curl -X POST 'http://localhost:3006/v1/issue-unlock-pass' \
-H 'Content-Type: application/json' \
--data '{
"credential_public_key": "BASE64_ENCODED_65_BYTE_SEC1_P256_PUBLIC_KEY",
"duration": 604800
}'