Fetching Public Keys and Verifying Signatures
The UMA SDK includes functionality for fetching and caching public keys from other VASPs, as well as signing requests and verifying signatures. The first signature to verify is handled by the receiving VASP in the lnurlp request. First, the receiving VASP needs to fetch the public key for the VASP that sent the request.
The UMA SDK caches public keys using an implementation of the
PublicKeyCache
interface. An InMemoryPublicKeyCache
implementation is provided for you in the SDK, but you might want to create your own persistent cache to save public keys in your own DB rather than just in memory. The interface is very simple:class IPublicKeyCache(ABC):
@abstractmethod
def fetch_public_key_for_vasp(self, vasp_domain: str) -> Optional[PubkeyResponse]:
# Fetches the public key entry for a VASP if in the cache, otherwise
# returns None.
pass
@abstractmethod
def add_public_key_for_vasp(
self, vasp_domain: str, public_key: PubkeyResponse
) -> None:
# Adds a public key entry for a VASP to the cache.
pass
@abstractmethod
def remove_public_key_for_vasp(self, vasp_domain: str) -> None:
# Removes a public key for a VASP from the cache.
pass
@abstractmethod
def clear(self) -> None:
# Clears the cache.
pass
Once we have a PublicKeyCache implementation, we can fetch the sending VASP's public keys:
# Before verifying a signature, make sure the request is an UMA request
# (rather than a regular lnurl request).
if not request.is_uma_request():
return
pub_keys = uma.fetch_public_key_for_vasp(request.vasp_domain, pubkey_cache)
This function automatically uses the cache if there's a valid public key entry for the VASP. If not, it will request the public keys from the path
/.well-known/lnurlpubkey
at the other VASP's domain. The response you get back has the following structure:@dataclass
class PubkeyResponse:
"""
PubkeyResponse is sent from a VASP to another VASP to provide its public keys.
It is the response to GET requests at `/.well-known/lnurlpubkey`.
Use the `get_signing_pubkey` and `get_encryption_pubkey` methods to get the
public keys.
"""
signing_cert_chain: Optional[List[x509.Certificate]]
"""The certificate chain used to verify signatures from a VASP."""
encryption_cert_chain: Optional[List[x509.Certificate]]
"""The certificate chain used to encrypt TR info sent to a VASP."""
signing_pubkey: Optional[bytes]
"""Used to verify signatures from a VASP."""
encryption_pubkey: Optional[bytes]
"""Used to encrypt TR info sent to a VASP."""
expiration_timestamp: Optional[datetime]
"""
Optional expiration_timestamp in seconds since epoch at which these pub keys
must be refreshed. It can be safely cached until this expiration (or forever
if null).
"""
def get_signing_pubkey(self) -> bytes:
# Use this method to get the signing public key.
def get_encryption_pubkey(self) -> bytes:
# Use this method to get the encryption public key.
As an optional validation step, once you get back public keys from another VASP, you can validate that these keys
indeed belong to the other VASP either through a third-party service or based on your own internal verification tools,
called the VASP Identity Authority in the diagram. This step is optional and any VASP ID Authority will provide APIs
or interfaces separate from UMA.
You'll also need to respond to public key requests yourself. To do so, add a route for GET requests at
/.well-known/lnurlpubkey
. Pick any expiration length you'd like and respond with a JSON-serialized PubKeyResponse
:# Assuming we're handling a route using FLASK:
from datetime import datetime, timedelta, timezone
from flask import Flask
from typing import Any
import uma
import os
app = Flask(__name__)
signing_cert_chain = os.environ.get("VASP_SIGNING_CERT_CHAIN")
encryption_cert_chain = os.environ.get("VASP_ENCRYPTION_CERT_CHAIN")
@app.route("/.well-known/lnurlpubkey")
def lnurlp_pubkey() -> dict[str, Any]:
two_weeks_from_now = datetime.now(timezone.utc) + timedelta(weeks=2)
return uma.create_pubkey_response(
signing_cert_chain=signing_cert_chain,
encryption_cert_chain=encryption_cert_chain,
expiration_timestamp=two_weeks_from_now,
).to_dict()
Now we can fetch, cache, and provide public keys as needed! Let's continue our journey to use the sending VASP's public key to verify its signature on the
LnurlpRequest
before responding.