1 Commits

Author SHA1 Message Date
CleverWild
5a34463228 security(server): bind grant revocation state (revoked_at) to integrity hash
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-08 12:09:54 +02:00
7 changed files with 27 additions and 54 deletions

View File

@@ -394,6 +394,7 @@ mod tests {
chain: CHAIN_ID,
valid_from: None,
valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
@@ -596,4 +597,24 @@ mod tests {
assert!(violations.is_empty());
}
}
#[test]
fn shared_settings_hash_changes_when_revoked_at_changes() {
use arbiter_crypto::hashing::Hashable;
use sha2::Digest;
let active = shared_settings();
let revoked = SharedGrantSettings {
revoked_at: Some(Utc::now()),
..shared_settings()
};
let mut active_hash = sha2::Sha256::new();
active.hash(&mut active_hash);
let mut revoked_hash = sha2::Sha256::new();
revoked.hash(&mut revoked_hash);
assert_ne!(active_hash.finalize(), revoked_hash.finalize());
}
}

View File

@@ -146,6 +146,7 @@ pub struct SharedGrantSettings {
pub valid_from: Option<DateTime<Utc>>,
pub valid_until: Option<DateTime<Utc>>,
pub revoked_at: Option<DateTime<Utc>>,
pub max_gas_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>,
@@ -160,6 +161,7 @@ impl SharedGrantSettings {
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
valid_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into),
revoked_at: model.revoked_at.map(Into::into),
max_gas_fee_per_gas: model
.max_gas_fee_per_gas
.map(|b| utils::try_bytes_to_u256(&b))

View File

@@ -78,6 +78,7 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID,
valid_from: None,
valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,

View File

@@ -95,6 +95,7 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID,
valid_from: None,
valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,

View File

@@ -87,6 +87,7 @@ impl TryConvert for ProtoSharedSettings {
.valid_until
.map(ProtoTimestamp::try_convert)
.transpose()?,
revoked_at: None,
max_gas_fee_per_gas: self
.max_gas_fee_per_gas
.as_deref()

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/features/connection/server_info_storage.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
import 'package:crypto/crypto.dart';
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
import 'package:arbiter/proto/user_agent.pb.dart';
@@ -46,18 +45,6 @@ class ConnectionException implements Exception {
String toString() => message;
}
String certificateFingerprintHex(List<int> derBytes) {
return sha256.convert(derBytes).toString();
}
bool isPinnedServerCertificate({
required String expectedFingerprint,
required List<int> certificateDer,
}) {
return certificateFingerprintHex(certificateDer) ==
expectedFingerprint.toLowerCase();
}
Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo,
KeyHandle key, {
@@ -168,12 +155,7 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
connectTimeout: const Duration(seconds: 10),
credentials: ChannelCredentials.secure(
onBadCertificate: (cert, host) {
final isExpectedHost = host == serverInfo.address;
final isPinnedCert = isPinnedServerCertificate(
expectedFingerprint: serverInfo.caCertFingerprint,
certificateDer: cert.der,
);
return isExpectedHost && isPinnedCert;
return true;
},
),
),

View File

@@ -1,35 +0,0 @@
import 'package:arbiter/features/connection/auth.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('certificate pinning helpers', () {
test('certificateFingerprintHex returns SHA-256 in hex', () {
final fingerprint = certificateFingerprintHex('abc'.codeUnits);
expect(
fingerprint,
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
);
});
test('isPinnedServerCertificate matches expected fingerprint', () {
final matches = isPinnedServerCertificate(
expectedFingerprint:
'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD',
certificateDer: 'abc'.codeUnits,
);
expect(matches, isTrue);
});
test('isPinnedServerCertificate rejects mismatched fingerprint', () {
final matches = isPinnedServerCertificate(
expectedFingerprint:
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
certificateDer: 'abc'.codeUnits,
);
expect(matches, isFalse);
});
});
}