diff options
| -rw-r--r-- | src/digiid.ts | 103 | 
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 +  }; +} | 
