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.