diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c19d7b
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index ac65080..5e9689f 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,8 @@
## Getting started
-To use this SDK, you first need login into to [blockfrost.io](https://blockfrost.io) create your project to retrive your API token.
+To use this SDK, you first need login into to [blockfrost.io](https://blockfrost.io) create your
+project to retrieve your API token.
@@ -31,29 +32,33 @@ Using the SDK is pretty straight-forward as you can see from the following examp
```dart
import 'package:blockfrost_api/blockfrost_api.dart';
-void main() async {
-
- String projectId = "";
-
- String out;
-
- try
- {
- BlockService service = BlockService(Service.networkCardanoMainnet, projectId);
-
- BlockContent block = await service.getLatestBlock();
-
- out = block.hash;
- }
-
- catch(e)
- {
- out = e.toString();
- }
-
- print(out);
+void main() async {
+ String projectId = "";
+ String out;
+
+ try {
+ BlockService service = BlockService(Service.networkCardanoMainnet, projectId);
+
+ BlockContent block = await service.getLatestBlock();
+
+ out = block.hash;
+ }
+
+ catch (e) {
+ out = e.toString();
+ }
+
+ print(out);
}
```
+# How to run the unit tests?
+
+## Note: Replace the placeholders by values from https://blockfrost.io/dashboard
+
+```
+PROJECT_ID_MAINNET="[YOUR_PROJECT_ID_MAINNET_HERE]" PROJECT_ID_IPFS="YOUR_PROJECT_IPFS_ID" dart run test test/
+```
+
diff --git a/lib/blockfrost_api.dart b/lib/blockfrost_api.dart
index 1e398ec..79fcb4b 100644
--- a/lib/blockfrost_api.dart
+++ b/lib/blockfrost_api.dart
@@ -15,5 +15,4 @@ export 'src/scripts_service.dart';
export 'src/transactions_service.dart';
export 'src/utilities_service.dart';
export 'src/ipfs_service.dart';
-
-
+export 'src/utils/utils_exports.dart';
diff --git a/lib/src/utils/blockfrost_signature_validator.dart b/lib/src/utils/blockfrost_signature_validator.dart
new file mode 100644
index 0000000..4968764
--- /dev/null
+++ b/lib/src/utils/blockfrost_signature_validator.dart
@@ -0,0 +1,124 @@
+import 'dart:convert';
+
+import 'package:blockfrost_api/src/utils/signature_validation_exception.dart';
+import 'package:blockfrost_api/src/utils/signature_validator.dart';
+import 'package:crypto/crypto.dart';
+
+/// Adapter class which implements the validator interface to validate the blockfrost webhook signature.
+class BlockfrostSignatureValidator implements SignatureValidator {
+ @override
+ bool validate({
+ required String requestPayload,
+ required String signatureHeader,
+ required String secretAuthToken,
+ int maxToleranceSeconds = defaultMaxToleranceSeconds,
+ }) {
+ return _validateSignature(
+ requestPayload: requestPayload,
+ signatureHeader: signatureHeader,
+ secretAuthToken: secretAuthToken,
+ maxToleranceSeconds: maxToleranceSeconds,
+ );
+ }
+}
+
+bool _validateSignature({
+ required String requestPayload,
+ required String signatureHeader,
+ required String secretAuthToken,
+ required int maxToleranceSeconds,
+ int? currentUnixTime,
+}) {
+ // Parse the timestamp and signature from the header
+ String? timestampString;
+ List 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) {
+ throw SignatureValidationException(
+ "Invalid signature header format.",
+ header: signatureHeader,
+ payload: requestPayload,
+ );
+ }
+
+ final int timestamp;
+ try {
+ timestamp = int.parse(timestampString);
+ } catch (e) {
+ throw SignatureValidationException(
+ "Invalid timestamp format.",
+ header: signatureHeader,
+ payload: requestPayload,
+ );
+ }
+
+ // 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) {
+ throw SignatureValidationException(
+ "No signature matches the expected signature for the payload.",
+ header: signatureHeader,
+ payload: requestPayload,
+ );
+ }
+
+ // Check timestamp tolerance (prevent replay attacks)
+ // Note: currentUnixTime can be injected for testing purposes
+ final currentTimestamp =
+ currentUnixTime ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
+ final timeDifference = (currentTimestamp - timestamp).abs();
+ if (timeDifference > maxToleranceSeconds) {
+ throw SignatureValidationException(
+ "Signature's timestamp is outside of the time tolerance.",
+ header: signatureHeader,
+ payload: requestPayload,
+ );
+ }
+ return true;
+}
+
+/// TESTING ACCESSOR: used only by unit test to access the private method
+class TestBlockfrostValidatorAccessor {
+ bool callValidateSignature({
+ required String requestPayload,
+ required String signatureHeader,
+ required String secretAuthToken,
+ required int currentUnixTime,
+ int maxToleranceSeconds = defaultMaxToleranceSeconds,
+ }) {
+ return _validateSignature(
+ requestPayload: requestPayload,
+ signatureHeader: signatureHeader,
+ secretAuthToken: secretAuthToken,
+ currentUnixTime: currentUnixTime,
+ maxToleranceSeconds: maxToleranceSeconds,
+ );
+ }
+}
diff --git a/lib/src/utils/signature_validation_exception.dart b/lib/src/utils/signature_validation_exception.dart
new file mode 100644
index 0000000..6a58452
--- /dev/null
+++ b/lib/src/utils/signature_validation_exception.dart
@@ -0,0 +1,18 @@
+class SignatureValidationException implements Exception {
+ final String message;
+ final String header;
+ final String payload;
+
+ SignatureValidationException(
+ this.message, {
+ required this.header,
+ required this.payload,
+ });
+
+ @override
+ String toString() {
+ return 'SignatureValidationException: $message\n'
+ 'Header: $header\n'
+ 'Payload: $payload';
+ }
+}
diff --git a/lib/src/utils/signature_validator.dart b/lib/src/utils/signature_validator.dart
new file mode 100644
index 0000000..17c028a
--- /dev/null
+++ b/lib/src/utils/signature_validator.dart
@@ -0,0 +1,11 @@
+// The interface for the signature validation
+const int defaultMaxToleranceSeconds = 600;
+
+abstract class SignatureValidator {
+ bool validate({
+ required String requestPayload,
+ required String signatureHeader,
+ required String secretAuthToken,
+ int maxToleranceSeconds,
+ });
+}
diff --git a/lib/src/utils/utils_exports.dart b/lib/src/utils/utils_exports.dart
new file mode 100644
index 0000000..e2f36cc
--- /dev/null
+++ b/lib/src/utils/utils_exports.dart
@@ -0,0 +1,4 @@
+// Export validator files
+export 'blockfrost_signature_validator.dart';
+export 'signature_validation_exception.dart';
+export 'signature_validator.dart';
diff --git a/pubspec.lock b/pubspec.lock
index 6e4c143..7ef4d24 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -5,330 +5,409 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- url: "https://pub.dartlang.org"
+ sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
+ url: "https://pub.dev"
source: hosted
- version: "47.0.0"
+ version: "88.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
- url: "https://pub.dartlang.org"
+ sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
+ url: "https://pub.dev"
source: hosted
- version: "4.7.0"
+ version: "8.1.1"
args:
dependency: transitive
description:
name: args
- url: "https://pub.dartlang.org"
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.7.0"
async:
dependency: transitive
description:
name: async
- url: "https://pub.dartlang.org"
+ sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
+ url: "https://pub.dev"
source: hosted
- version: "2.9.0"
+ version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
- url: "https://pub.dartlang.org"
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.2"
+ cli_config:
+ dependency: transitive
+ description:
+ name: cli_config
+ sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
- url: "https://pub.dartlang.org"
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
- url: "https://pub.dartlang.org"
+ sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
+ url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
- url: "https://pub.dartlang.org"
+ sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
+ url: "https://pub.dev"
source: hosted
- version: "1.6.0"
+ version: "1.15.0"
crypto:
- dependency: transitive
+ dependency: "direct main"
description:
name: crypto
- url: "https://pub.dartlang.org"
+ sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
+ url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.0.6"
file:
dependency: transitive
description:
name: file
- url: "https://pub.dartlang.org"
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+ url: "https://pub.dev"
source: hosted
- version: "6.1.4"
+ version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
- url: "https://pub.dartlang.org"
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+ url: "https://pub.dev"
source: hosted
- version: "3.0.0"
+ version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
- url: "https://pub.dartlang.org"
+ sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.3"
http:
dependency: "direct main"
description:
name: http
- url: "https://pub.dartlang.org"
+ sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
+ url: "https://pub.dev"
source: hosted
- version: "0.13.5"
+ version: "0.13.6"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
- url: "https://pub.dartlang.org"
+ sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
+ url: "https://pub.dev"
source: hosted
- version: "3.2.1"
+ version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
- url: "https://pub.dartlang.org"
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.dev"
source: hosted
- version: "4.0.1"
+ version: "4.1.2"
io:
dependency: transitive
description:
name: io
- url: "https://pub.dartlang.org"
+ sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
+ url: "https://pub.dev"
source: hosted
- version: "1.0.3"
+ version: "1.0.5"
js:
dependency: transitive
description:
name: js
- url: "https://pub.dartlang.org"
+ sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
+ url: "https://pub.dev"
source: hosted
- version: "0.6.4"
+ version: "0.7.2"
lints:
dependency: "direct dev"
description:
name: lints
- url: "https://pub.dartlang.org"
+ sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
+ url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
- url: "https://pub.dartlang.org"
+ sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+ url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
- url: "https://pub.dartlang.org"
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ url: "https://pub.dev"
source: hosted
- version: "0.12.12"
+ version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
- url: "https://pub.dartlang.org"
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
- url: "https://pub.dartlang.org"
+ sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ mocktail:
+ dependency: "direct dev"
+ description:
+ name: mocktail
+ sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
+ url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "1.0.4"
node_preamble:
dependency: transitive
description:
name: node_preamble
- url: "https://pub.dartlang.org"
+ sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.1"
+ version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
- url: "https://pub.dartlang.org"
+ sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.2.0"
path:
dependency: transitive
description:
name: path
- url: "https://pub.dartlang.org"
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.2"
+ version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
- url: "https://pub.dartlang.org"
+ sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
+ url: "https://pub.dev"
source: hosted
- version: "1.5.1"
+ version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
- url: "https://pub.dartlang.org"
+ sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
- url: "https://pub.dartlang.org"
+ sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
+ url: "https://pub.dev"
source: hosted
- version: "1.3.2"
+ version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
- url: "https://pub.dartlang.org"
+ sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
+ url: "https://pub.dev"
source: hosted
- version: "3.0.1"
+ version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
- url: "https://pub.dartlang.org"
+ sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
+ url: "https://pub.dev"
source: hosted
- version: "1.1.1"
+ version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
- url: "https://pub.dartlang.org"
+ sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
+ url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
- url: "https://pub.dartlang.org"
+ sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
- url: "https://pub.dartlang.org"
+ sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
+ url: "https://pub.dev"
source: hosted
- version: "0.10.10"
+ version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
- url: "https://pub.dartlang.org"
+ sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+ url: "https://pub.dev"
source: hosted
- version: "1.9.1"
+ version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
- url: "https://pub.dartlang.org"
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.dev"
source: hosted
- version: "1.10.0"
+ version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
- url: "https://pub.dartlang.org"
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
- url: "https://pub.dartlang.org"
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+ url: "https://pub.dev"
source: hosted
- version: "1.1.1"
+ version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
- url: "https://pub.dartlang.org"
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+ url: "https://pub.dev"
source: hosted
- version: "1.2.1"
+ version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
- url: "https://pub.dartlang.org"
+ sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
+ url: "https://pub.dev"
source: hosted
- version: "1.21.5"
+ version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
- url: "https://pub.dartlang.org"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ url: "https://pub.dev"
source: hosted
- version: "0.4.13"
+ version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
- url: "https://pub.dartlang.org"
+ sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
+ url: "https://pub.dev"
source: hosted
- version: "0.4.17"
+ version: "0.6.12"
typed_data:
dependency: transitive
description:
name: typed_data
- url: "https://pub.dartlang.org"
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
source: hosted
- version: "1.3.1"
+ version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
- url: "https://pub.dartlang.org"
+ sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
+ url: "https://pub.dev"
source: hosted
- version: "9.4.0"
+ version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
- url: "https://pub.dartlang.org"
+ sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.4"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ web_socket:
+ dependency: transitive
+ description:
+ name: web_socket
+ sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
+ url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
- url: "https://pub.dartlang.org"
+ sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
+ url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
- url: "https://pub.dartlang.org"
+ sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
+ url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
- url: "https://pub.dartlang.org"
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
source: hosted
- version: "3.1.1"
+ version: "3.1.3"
sdks:
- dart: ">=2.18.0 <3.0.0"
+ dart: ">=3.7.0 <4.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 59d87f1..d49be49 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,16 +4,16 @@ version: 1.0.0
# homepage: https://www.example.com
environment:
- sdk: '>=2.18.0 <3.0.0'
+ sdk: '>=3.0.0 <4.0.0'
-# dependencies:
-# path: ^1.8.0
-
-dev_dependencies:
- lints: ^2.0.0
- test: ^1.16.0
dependencies:
http: ^0.13.5
+ crypto: ^3.0.3
+
+dev_dependencies:
+ lints: ^5.0.0
+ test: ^1.24.0
+ mocktail: ^1.0.0
\ No newline at end of file
diff --git a/test/utils/blockfrost_signature_validator_test.dart b/test/utils/blockfrost_signature_validator_test.dart
new file mode 100644
index 0000000..5ffc1e7
--- /dev/null
+++ b/test/utils/blockfrost_signature_validator_test.dart
@@ -0,0 +1,204 @@
+import 'dart:convert';
+
+import 'package:blockfrost_api/blockfrost_api.dart';
+import 'package:crypto/crypto.dart';
+import 'package:test/test.dart';
+
+const maxToleranceSeconds = 600;
+
+// --- Test Constants ---
+const testSecret = 'TEST_SECRET';
+const testPayload = '{"event":"test_event", "data": "content"}';
+
+// Helper function to generate a valid signature for tests
+String generateSignature(int timestamp, String payload, String secret) {
+ final signaturePayload = '$timestamp.$payload';
+ final key = utf8.encode(secret);
+ final messageBytes = utf8.encode(signaturePayload);
+
+ final hmac = Hmac(sha256, key);
+ final digest = hmac.convert(messageBytes);
+ return digest.toString();
+}
+
+void main() {
+ group('Blockfrost Webhook Signature Validation', () {
+ final int standardTimestamp = 1759917705;
+ final validator = TestBlockfrostValidatorAccessor();
+
+ // Generate a valid signature string for the standard time and payload
+ final String validSignature = generateSignature(
+ standardTimestamp,
+ testPayload,
+ testSecret,
+ );
+
+ // Signature header
+ final String validHeader = 't=$standardTimestamp,v1=$validSignature';
+
+ // --- SUCCESS CASE ---
+ test('Should return true for a valid, recent signature', () {
+ final result = validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: validHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: standardTimestamp,
+ );
+ expect(result, isTrue);
+ });
+
+ // --- FAILURE CASES ---
+ test(
+ 'Should throw SignatureValidationException if signatureHeader is malformed (missing t or v1)',
+ () {
+ // Case 1: Missing timestamp
+ expect(
+ () => validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: 'v1=$validSignature',
+ secretAuthToken: testSecret,
+ currentUnixTime: standardTimestamp,
+ ),
+ throwsA(
+ predicate(
+ (e) =>
+ e is SignatureValidationException &&
+ e.message.contains('Invalid signature header format.'),
+ 'SignatureValidationException with correct message',
+ ),
+ ),
+ reason: 'Should throw exception for malformed header',
+ );
+ });
+
+ test(
+ 'Should throw SignatureValidationException if timestamp format is invalid',
+ () {
+ final invalidHeader = 't=mywrongtimestamp,v1=$validSignature';
+ expect(
+ () => validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: invalidHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: standardTimestamp,
+ ),
+ throwsA(
+ predicate(
+ (e) =>
+ e is SignatureValidationException &&
+ e.message.contains('Invalid timestamp format.'),
+ 'SignatureValidationException with correct message',
+ ),
+ ),
+ reason: 'Should throw exception for invalid timestamp');
+ });
+
+ test(
+ 'Should throw SignatureValidationException if the signature does not match',
+ () {
+ // 1. Wrong Secret (Use a different secret to calculate the expected signature)
+ final badSignature = generateSignature(
+ standardTimestamp,
+ testPayload,
+ 'my_wrong_secret',
+ );
+ final badHeader = 't=$standardTimestamp,v1=$badSignature';
+
+ // Validate using the correct secret
+ expect(
+ () => validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: badHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: standardTimestamp,
+ ),
+ throwsA(
+ predicate(
+ (e) =>
+ e is SignatureValidationException &&
+ e.message.contains(
+ 'No signature matches the expected signature for the payload.'),
+ 'SignatureValidationException with correct message',
+ ),
+ ),
+ reason: 'Should throw exception for not matching signatures');
+ });
+
+ test(
+ 'Should throw SignatureValidationException if the timestamp is too far in the future',
+ () {
+ final farFutureTime = standardTimestamp + maxToleranceSeconds + 1;
+ print("$farFutureTime");
+
+ expect(
+ () => validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: validHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime:
+ farFutureTime, // Inject current time far in the future
+ ),
+ throwsA(
+ predicate(
+ (e) =>
+ e is SignatureValidationException &&
+ e.message.contains(
+ 'Signature\'s timestamp is outside of the time tolerance.'),
+ 'SignatureValidationException with correct message',
+ ),
+ ),
+ reason: 'Should throw exception for not matching signatures');
+ });
+
+ test(
+ 'Should throw SignatureValidationException if the timestamp is too old (replay attack)',
+ () {
+ final farPastTime = standardTimestamp - maxToleranceSeconds - 1;
+ print("$farPastTime");
+ expect(
+ () => validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: validHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: farPastTime,
+ ),
+ throwsA(
+ predicate(
+ (e) =>
+ e is SignatureValidationException &&
+ e.message.contains(
+ 'Signature\'s timestamp is outside of the time tolerance.'),
+ 'SignatureValidationException with correct message',
+ ),
+ ),
+ reason: 'Should throw exception for not matching signatures');
+ });
+
+ test('Should return true if time difference is exactly the tolerance limit',
+ () {
+ final toleranceLimitTime = standardTimestamp + maxToleranceSeconds;
+ final result = validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: validHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: toleranceLimitTime,
+ );
+ expect(result, isTrue);
+ });
+
+ test(
+ 'Should return true for custom maxToleranceSeconds if time difference is exactly the tolerance limit',
+ () {
+ int customMaxToleranceSeconds = 60;
+ final toleranceLimitTime = standardTimestamp + customMaxToleranceSeconds;
+ final result = validator.callValidateSignature(
+ requestPayload: testPayload,
+ signatureHeader: validHeader,
+ secretAuthToken: testSecret,
+ currentUnixTime: toleranceLimitTime,
+ maxToleranceSeconds: customMaxToleranceSeconds,
+ );
+ expect(result, isTrue);
+ });
+ });
+}