From 81931d914b0edd6be21e4c03d33f64d71af99267 Mon Sep 17 00:00:00 2001
From: Pawel Zelawski <pawel.zelawski@outlook.com>
Date: Wed, 9 Apr 2025 19:24:26 +0200
Subject: 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.
---
 src/digiid.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 102 insertions(+), 1 deletion(-)

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
+  };
+}
-- 
cgit v1.2.3