Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
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
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="https://i.imgur.com/smY12ro.png">

Expand All @@ -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 = "<insert project id>";

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 = "<insert project id>";

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/
```


3 changes: 1 addition & 2 deletions lib/blockfrost_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
115 changes: 115 additions & 0 deletions lib/src/utils/blockfrost_signature_validator.dart
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;

Copy link
Copy Markdown

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.validate with 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


/// Adapter class which implements the validator interface to validate the blockfrost webhook signature.
class BlockfrostSignatureValidator implements SignatureValidator {
@override
bool validate({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 anyone with log access sees the expected signature for the payload, they can replay the request within the tolerance window by sending the same body with t and v1 values from the logs.

If possible throw an error similiar to python/node.js implementation with signature header and payload attached. Don't print expected signature.
https://github.com/blockfrost/blockfrost-js/blob/master/src/utils/helpers.ts#L285-L291

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, please throw an error instead of using print fn

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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,
);
}
}
8 changes: 8 additions & 0 deletions lib/src/utils/signature_validator.dart
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,
});
}
3 changes: 3 additions & 0 deletions lib/src/utils/utils_exports.dart
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';
Loading