summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
+ };
+}