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:export interface PublicKeyCache {
fetchPublicKeyForVasp(vaspDomain: string): PubKeyResponse | undefined;
addPublicKeyForVasp(vaspDomain: string, pubKey: PubKeyResponse): void;
removePublicKeyForVasp(vaspDomain: string): void;
clear(): void;
}
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). All signed messages have some equivalent
// of this check.
if (!isLnurlpRequestForUma(request)) {
return;
}
let pubKeys: uma.PubKeyResponse;
try {
pubKeys = await uma.fetchPublicKeyForVasp({
cache: this.pubKeyCache,
vaspDomain: request.vaspDomain,
});
} catch (e) {
console.error(e);
response.status(424).send("Failed to fetch public keys.");
return;
}
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:/**
* 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`.
*/
export class PubKeyResponse {
constructor(
/**
* SigningCertChain is a PEM encoded X.509 certificate chain. Certificates
* are ordered from leaf to root. The signing public key can be extracted from
* the leaf certificate and is used to verify signatures from a VASP.
*/
public readonly signingCertChain?: X509Certificate[] | undefined,
/**
* EncryptionCertChain is a PEM encoded X.509 certificate chain. Certificates
* are ordered from leaf to root. The encryption public key can be extracted
* from the leaf certificate and is used to verify signatures from a VASP.
*/
public readonly encryptionCertChain?: X509Certificate[] | undefined,
/** SigningPubKey is used to verify signatures from a VASP. */
public readonly signingPubKey?: string | undefined,
/** EncryptionPubKey is used to encrypt TR info sent to a VASP. */
public readonly encryptionPubKey?: string | undefined,
/**
* [Optional] Seconds since epoch at which these pub keys must be refreshed.
* They can be safely cached until this expiration (or forever if null).
*/
public readonly expirationTimestamp?: number | undefined,
) {}
// Use this method to get the signing public key.
getSigningPubKey(): Uint8Array
// Use this method to get the encryption public key.
getEncryptionPubKey(): Uint8Array
}
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
:import { getPubKeyResponse } from "@uma-sdk/core";
const twoWeeksFromNow = Math.floor(Date.now() / 1000) + 14 * 24 * 60 * 60;
// Note: The cert chains here are the chain of certificates in PEM format.
// The order of the certificates should be from the leaf to the root. When the cert
// is not obtained from a VASP ID authority, there will only be one self-signed
// certificate in the chain.
const response = getPubKeyResponse({
signingCertChainPem: signingCertChainString,
encryptionCertChainPem: encryptionCertChainString,
expirationTimestamp: twoWeeksFromNow,
});
response.send(response.toJsonString());
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.