diff options
author | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-09 19:24:26 +0200 |
---|---|---|
committer | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-09 19:24:26 +0200 |
commit | 81931d914b0edd6be21e4c03d33f64d71af99267 (patch) | |
tree | e413b24aa17224aa08b4f6066a7811dc370cf400 /src/digiid.ts | |
parent | 8092ceaf10dd5951d0b5011fc8d5a05b49335a6e (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/digiid.ts')
-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 + }; +} |