-
Notifications
You must be signed in to change notification settings - Fork 1
feature: SignatureValidation #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # https://dart.dev/guides/libraries/private-files | ||
|
|
||
| # Don't commit the following files and directories created by pub | ||
| .dart_tool/ | ||
| build/ | ||
|
|
||
| # Don't commit the API documentation directory created by dart doc | ||
| doc/api/ | ||
|
|
||
| # Don't commit files and directories created by other development environments | ||
| ## IntelliJ | ||
| *.iml | ||
| *.ipr | ||
| *.iws | ||
| .idea/ | ||
|
|
||
| ## Mac | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| import 'package:blockfrost_api/src/utils/signature_validator.dart'; | ||
| import 'package:crypto/crypto.dart'; | ||
|
|
||
| const int maxToleranceSeconds = 60; | ||
|
|
||
| /// Adapter class which implements the validator interface to validate the blockfrost webhook signature. | ||
| class BlockfrostSignatureValidator implements SignatureValidator { | ||
| @override | ||
| bool validate({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please change the order of the params to match the node.js, python and ruby SDK implementation https://github.com/blockfrost/blockfrost-python/blob/master/blockfrost/helpers.py#L18 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
| required String signatureHeader, | ||
| required String requestPayload, | ||
| required String secretAuthToken, | ||
| }) { | ||
| return _validateSignature( | ||
| signatureHeader: signatureHeader, | ||
| requestPayload: requestPayload, | ||
| secretAuthToken: secretAuthToken, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| bool _validateSignature({ | ||
| required String signatureHeader, | ||
| required String requestPayload, // JSON payload as raw string | ||
| required String secretAuthToken, | ||
| int? currentUnixTime, | ||
| }) { | ||
| // Parse the timestamp and signature from the header | ||
| String? timestampString; | ||
| List<String> providedSignatures = []; | ||
|
|
||
| final parts = signatureHeader.split(','); | ||
| for (final part in parts) { | ||
| final pair = part.trim().split('='); | ||
| if (pair.length == 2) { | ||
| final key = pair[0]; | ||
| final value = pair[1]; | ||
| if (key == 't') { | ||
| timestampString = value; | ||
| } else if (key == 'v1') { | ||
| providedSignatures.add(value); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (timestampString == null || providedSignatures.isEmpty) { | ||
| print('Validation Failed: Missing timestamp (t) or (v1) signature.'); | ||
| return false; | ||
| } | ||
|
|
||
| final int timestamp; | ||
| try { | ||
| timestamp = int.parse(timestampString); | ||
| } catch (e) { | ||
| print('Validation Failed: Invalid timestamp format.'); | ||
| return false; | ||
| } | ||
|
|
||
| // Prepare the signature_payload (timestamp.payload) | ||
| final signaturePayload = '$timestampString.$requestPayload'; | ||
|
|
||
| // Compute the expected signature (HMAC-SHA256) | ||
| final key = utf8.encode(secretAuthToken); | ||
| final messageBytes = utf8.encode(signaturePayload); | ||
|
|
||
| final hmac = Hmac(sha256, key); | ||
| final digest = hmac.convert(messageBytes); | ||
| final expectedSignature = digest.toString(); | ||
|
|
||
| // Check for matching signature | ||
| bool signatureMatch = | ||
| providedSignatures.any((sig) => sig == expectedSignature); | ||
|
|
||
| if (!signatureMatch) { | ||
| print('Verification Failed: Signatures do not match.'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaking the expected HMAC = leaking an auth token for that payload+timestamp. If possible throw an error similiar to python/node.js implementation with signature header and payload attached. Don't print expected signature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
| print('Expected: $expectedSignature'); | ||
| print('Provided: $providedSignatures'); | ||
| return false; | ||
| } | ||
|
|
||
| // Check timestamp tolerance (prevent replay attacks) | ||
| // Note: we might also inject time for testing purposes | ||
| final currentTimestamp = currentUnixTime ?? | ||
| (DateTime.now().millisecondsSinceEpoch ~/ 1000); // Unix time in seconds | ||
|
|
||
| print("currentTimestamp: $currentTimestamp"); | ||
| final timeDifference = (currentTimestamp - timestamp).abs(); | ||
| print("timeDifference: $timeDifference"); | ||
|
|
||
| if (timeDifference > maxToleranceSeconds) { | ||
| print( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above, please throw an error instead of using print fn There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
| 'Validation Failed: Timestamp too old/far ($timeDifference s difference).'); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /// TESTING ACCESSOR: used only by unit test to access the private method | ||
| class TestBlockfrostValidatorAccessor { | ||
| bool callValidateSignature({ | ||
| required String signatureHeader, | ||
| required String requestPayload, | ||
| required String secretAuthToken, | ||
| required int currentUnixTime, | ||
| }) { | ||
| return _validateSignature( | ||
| signatureHeader: signatureHeader, | ||
| requestPayload: requestPayload, | ||
| secretAuthToken: secretAuthToken, | ||
| currentUnixTime: currentUnixTime, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // The interface for the signature validation | ||
| abstract class SignatureValidator { | ||
| bool validate({ | ||
| required String signatureHeader, | ||
| required String requestPayload, | ||
| required String secretAuthToken, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| // Export validator files | ||
| export 'blockfrost_signature_validator.dart'; | ||
| export 'signature_validator.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please make this configurable by using it as a fn param in
BlockfrostSignatureValidator.validatewith default of 600s. It should mimic TS/Python interface as closest as possible.https://github.com/blockfrost/blockfrost-js/blob/master/src/utils/helpers.ts#L211
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed