summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPawel Zelawski <pawel.zelawski@outlook.com>2025-04-09 19:24:26 +0200
committerPawel Zelawski <pawel.zelawski@outlook.com>2025-04-09 19:24:26 +0200
commit81931d914b0edd6be21e4c03d33f64d71af99267 (patch)
treee413b24aa17224aa08b4f6066a7811dc370cf400 /src
parent8092ceaf10dd5951d0b5011fc8d5a05b49335a6e (diff)
feat: Implement DigiID callback verification
- Add the `verifyDigiIDCallback` async function to `src/digiid.ts`. - Parses the received DigiID URI to extract nonce, callback details, and unsecure flag. - Validates the callback URL path and scheme against expected values. - Optionally validates the received nonce against the expected nonce. - Utilizes the 'digibyte-message' library (via require) to perform cryptographic signature verification of the URI against the provided address. - Throws specific `DigiIDError` exceptions for various validation and verification failures (e.g., invalid URI, URL mismatch, nonce mismatch, invalid signature). - Returns a `DigiIDVerificationResult` upon successful verification. - Imports necessary types and the CommonJS `digibyte-message` dependency.
Diffstat (limited to 'src')
-rw-r--r--src/digiid.ts103
1 files changed, 102 insertions, 1 deletions
diff --git a/src/digiid.ts b/src/digiid.ts
index 6322403..5afabde 100644
--- a/src/digiid.ts
+++ b/src/digiid.ts
@@ -1,5 +1,15 @@
import { randomBytes } from 'crypto';
-import { DigiIDUriOptions, DigiIDError } from './types';
+import {
+ DigiIDUriOptions,
+ DigiIDError,
+ DigiIDCallbackData,
+ DigiIDVerifyOptions,
+ DigiIDVerificationResult
+} from './types';
+
+// Use require for the CommonJS dependency installed from Git
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const Message = require('digibyte-message');
/**
* Generates a secure random nonce (hex string).
@@ -53,3 +63,94 @@ export function generateDigiIDUri(options: DigiIDUriOptions): string {
return uri;
}
+
+/**
+ * Verifies the signature and data received from a DigiID callback.
+ *
+ * @param callbackData - The data received from the wallet (address, uri, signature).
+ * @param verifyOptions - Options for verification, including the expected callback URL and nonce.
+ * @returns {Promise<DigiIDVerificationResult>} A promise that resolves with verification details if successful.
+ * @throws {DigiIDError} If validation or signature verification fails.
+ */
+export async function verifyDigiIDCallback(
+ callbackData: DigiIDCallbackData,
+ verifyOptions: DigiIDVerifyOptions
+): Promise<DigiIDVerificationResult> {
+ const { address, uri, signature } = callbackData;
+ const { expectedCallbackUrl, expectedNonce } = verifyOptions;
+
+ if (!address || !uri || !signature) {
+ throw new DigiIDError('Missing required callback data: address, uri, or signature.');
+ }
+
+ // 1. Parse the received URI
+ let parsedReceivedUri: URL;
+ try {
+ // Temporarily replace digiid:// with http:// for standard URL parsing
+ const parsableUri = uri.replace(/^digiid:/, 'http:');
+ parsedReceivedUri = new URL(parsableUri);
+ } catch (e) {
+ throw new DigiIDError(`Invalid URI received in callback: ${(e as Error).message}`);
+ }
+
+ const receivedNonce = parsedReceivedUri.searchParams.get('x');
+ const receivedUnsecure = parsedReceivedUri.searchParams.get('u'); // 0 or 1
+ const receivedDomainAndPath = parsedReceivedUri.host + parsedReceivedUri.pathname;
+
+ if (receivedNonce === null || receivedUnsecure === null) {
+ throw new DigiIDError('URI missing nonce (x) or unsecure (u) parameter.');
+ }
+
+ // 2. Validate Callback URL
+ let parsedExpectedUrl: URL;
+ try {
+ // Allow expectedCallbackUrl to be a string or URL object
+ parsedExpectedUrl = typeof expectedCallbackUrl === 'string' ? new URL(expectedCallbackUrl) : expectedCallbackUrl;
+ } catch (e) {
+ throw new DigiIDError(`Invalid expectedCallbackUrl provided: ${(e as Error).message}`);
+ }
+
+ const expectedDomainAndPath = parsedExpectedUrl.host + parsedExpectedUrl.pathname;
+
+ if (receivedDomainAndPath !== expectedDomainAndPath) {
+ throw new DigiIDError(`Callback URL mismatch: URI contained "${receivedDomainAndPath}", expected "${expectedDomainAndPath}"`);
+ }
+
+ // Validate scheme consistency
+ const expectedScheme = parsedExpectedUrl.protocol;
+ if (receivedUnsecure === '1' && expectedScheme !== 'http:') {
+ throw new DigiIDError('URI indicates unsecure (u=1), but expectedCallbackUrl is not http.');
+ }
+ if (receivedUnsecure === '0' && expectedScheme !== 'https:') {
+ throw new DigiIDError('URI indicates secure (u=0), but expectedCallbackUrl is not https.');
+ }
+
+ // 3. Validate Nonce (optional)
+ if (expectedNonce && receivedNonce !== expectedNonce) {
+ throw new DigiIDError(`Nonce mismatch: URI contained "${receivedNonce}", expected "${expectedNonce}". Possible replay attack.`);
+ }
+
+ // 4. Verify Signature using digibyte-message
+ try {
+ // The bitcore-message standard expects the message string, address, and signature.
+ // The message signed is the full DigiID URI string.
+ const messageInstance = new Message(uri);
+ // The verify method might be synchronous or asynchronous depending on the underlying lib
+ // Assuming synchronous based on common bitcore patterns, but wrapping for safety
+ const isValidSignature = await Promise.resolve(messageInstance.verify(address, signature));
+
+ if (!isValidSignature) {
+ throw new DigiIDError('Invalid signature.');
+ }
+ } catch (e: any) {
+ // Catch potential errors from the verify function (e.g., invalid address/signature format)
+ throw new DigiIDError(`Signature verification failed: ${e.message || e}`);
+ }
+
+ // 5. Return successful result
+ return {
+ isValid: true,
+ address: address,
+ nonce: receivedNonce, // Return the nonce from the URI
+ };
+}