improve route selection and add generate_shared_secret

This commit is contained in:
Christien Rioux 2024-03-18 10:10:10 -04:00
parent d49e78d931
commit 800348451e
18 changed files with 317 additions and 6 deletions

View File

@ -1,5 +1,7 @@
use super::*;
const VEILID_DOMAIN_API: &[u8] = b"VEILID_API";
pub trait CryptoSystem {
// Accessors
fn kind(&self) -> CryptoKind;
@ -17,6 +19,15 @@ pub trait CryptoSystem {
fn random_nonce(&self) -> Nonce;
fn random_shared_secret(&self) -> SharedSecret;
fn compute_dh(&self, key: &PublicKey, secret: &SecretKey) -> VeilidAPIResult<SharedSecret>;
fn generate_shared_secret(
&self,
key: &PublicKey,
secret: &SecretKey,
domain: &[u8],
) -> VeilidAPIResult<SharedSecret> {
let dh = self.compute_dh(key, secret)?;
Ok(self.generate_hash(&[&dh.bytes, domain, VEILID_DOMAIN_API].concat()))
}
fn generate_keypair(&self) -> KeyPair;
fn generate_hash(&self, data: &[u8]) -> HashDigest;
fn generate_hash_reader(&self, reader: &mut dyn std::io::Read) -> VeilidAPIResult<HashDigest>;

View File

@ -849,8 +849,7 @@ impl RouteSpecStore {
// Get all valid routes, allow routes that need testing
// but definitely prefer routes that have been recently tested
for (id, rssd) in inner.content.iter_details() {
if rssd.get_stability() >= stability
&& rssd.is_sequencing_match(sequencing)
if rssd.is_sequencing_match(sequencing)
&& rssd.hop_count() >= min_hop_count
&& rssd.hop_count() <= max_hop_count
&& rssd.get_directions().is_superset(directions)
@ -864,6 +863,7 @@ impl RouteSpecStore {
// Sort the routes by preference
routes.sort_by(|a, b| {
// Prefer routes that don't need testing
let a_needs_testing = a.1.get_stats().needs_testing(cur_ts);
let b_needs_testing = b.1.get_stats().needs_testing(cur_ts);
if !a_needs_testing && b_needs_testing {
@ -872,6 +872,18 @@ impl RouteSpecStore {
if !b_needs_testing && a_needs_testing {
return cmp::Ordering::Greater;
}
// Prefer routes that meet the stability selection
let a_meets_stability = a.1.get_stability() >= stability;
let b_meets_stability = b.1.get_stability() >= stability;
if a_meets_stability && !b_meets_stability {
return cmp::Ordering::Less;
}
if b_meets_stability && !a_meets_stability {
return cmp::Ordering::Greater;
}
// Prefer faster routes
let a_latency = a.1.get_stats().latency_stats().average;
let b_latency = b.1.get_stats().latency_stats().average;

View File

@ -30,6 +30,15 @@ pub enum CryptoSystemRequestOp {
#[schemars(with = "String")]
secret: SecretKey,
},
GenerateSharedSecret {
#[schemars(with = "String")]
key: PublicKey,
#[schemars(with = "String")]
secret: SecretKey,
#[serde(with = "as_human_base64")]
#[schemars(with = "String")]
domain: Vec<u8>,
},
RandomBytes {
len: u32,
},
@ -151,6 +160,11 @@ pub enum CryptoSystemResponseOp {
#[schemars(with = "ApiResult<String>")]
result: ApiResultWithString<SharedSecret>,
},
GenerateSharedSecret {
#[serde(flatten)]
#[schemars(with = "ApiResult<String>")]
result: ApiResultWithString<SharedSecret>,
},
RandomBytes {
#[serde(with = "as_human_base64")]
#[schemars(with = "String")]

View File

@ -468,6 +468,16 @@ impl JsonRequestProcessor {
CryptoSystemRequestOp::ComputeDh { key, secret } => CryptoSystemResponseOp::ComputeDh {
result: to_json_api_result_with_string(csv.compute_dh(&key, &secret)),
},
CryptoSystemRequestOp::GenerateSharedSecret {
key,
secret,
domain,
} => CryptoSystemResponseOp::GenerateSharedSecret {
result: to_json_api_result_with_string(
csv.generate_shared_secret(&key, &secret, &domain),
),
},
CryptoSystemRequestOp::RandomBytes { len } => CryptoSystemResponseOp::RandomBytes {
value: csv.random_bytes(len),
},

View File

@ -32,3 +32,28 @@ Future<void> testHashAndVerifyPassword() async {
await cs.hashPassword(utf8.encode("abc1234"), salt);
expect(await cs.verifyPassword(utf8.encode("abc1235"), phash), isFalse);
}
Future<void> testGenerateSharedSecret() async {
final cs = await Veilid.instance.bestCryptoSystem();
final kp1 = await cs.generateKeyPair();
final kp2 = await cs.generateKeyPair();
final kp3 = await cs.generateKeyPair();
final ssA =
await cs.generateSharedSecret(kp1.key, kp2.secret, utf8.encode("abc123"));
final ssB =
await cs.generateSharedSecret(kp2.key, kp1.secret, utf8.encode("abc123"));
expect(ssA, equals(ssB));
final ssC = await cs.generateSharedSecret(
kp2.key, kp1.secret, utf8.encode("abc1234"));
expect(ssA, isNot(equals(ssC)));
final ssD =
await cs.generateSharedSecret(kp3.key, kp1.secret, utf8.encode("abc123"));
expect(ssA, isNot(equals(ssD)));
}

View File

@ -156,6 +156,8 @@ abstract class VeilidCryptoSystem {
CryptoKind kind();
Future<SharedSecret> cachedDH(PublicKey key, SecretKey secret);
Future<SharedSecret> computeDH(PublicKey key, SecretKey secret);
Future<SharedSecret> generateSharedSecret(
PublicKey key, SecretKey secret, Uint8List domain);
Future<Uint8List> randomBytes(int len);
Future<int> defaultSaltLength();
Future<String> hashPassword(Uint8List password, Uint8List salt);

View File

@ -163,6 +163,10 @@ typedef _CryptoCachedDHDart = void Function(
// fn crypto_compute_dh(port: i64, kind: u32, key: FfiStr, secret: FfiStr)
typedef _CryptoComputeDHDart = void Function(
int, int, Pointer<Utf8>, Pointer<Utf8>);
// fn crypto_generate_shared_secret(port: i64, kind: u32, key: FfiStr,
// secret: FfiStr, domain: FfiStr)
typedef _CryptoGenerateSharedSecretDart = void Function(
int, int, Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>);
// fn crypto_random_bytes(port: i64, kind: u32, len: u32)
typedef _CryptoRandomBytesDart = void Function(int, int, int);
// fn crypto_default_salt_length(port: i64, kind: u32)
@ -998,6 +1002,20 @@ class VeilidCryptoSystemFFI extends VeilidCryptoSystem {
return processFutureJson(SharedSecret.fromJson, recvPort.first);
}
@override
Future<SharedSecret> generateSharedSecret(
PublicKey key, SecretKey secret, Uint8List domain) async {
final nativeKey = jsonEncode(key).toNativeUtf8();
final nativeSecret = jsonEncode(secret).toNativeUtf8();
final nativeDomain = base64UrlNoPadEncode(domain).toNativeUtf8();
final recvPort = ReceivePort('crypto_generate_shared_secret');
final sendPort = recvPort.sendPort;
_ffi._cryptoGenerateSharedSecret(
sendPort.nativePort, _kind, nativeKey, nativeSecret, nativeDomain);
return processFutureJson(SharedSecret.fromJson, recvPort.first);
}
@override
Future<Uint8List> randomBytes(int len) async {
final recvPort = ReceivePort('crypto_random_bytes');
@ -1377,6 +1395,10 @@ class VeilidFFI extends Veilid {
_cryptoComputeDH = dylib.lookupFunction<
Void Function(Int64, Uint32, Pointer<Utf8>, Pointer<Utf8>),
_CryptoComputeDHDart>('crypto_compute_dh'),
_cryptoGenerateSharedSecret = dylib.lookupFunction<
Void Function(
Int64, Uint32, Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>),
_CryptoGenerateSharedSecretDart>('crypto_generate_shared_secret'),
_cryptoRandomBytes = dylib.lookupFunction<
Void Function(Int64, Uint32, Uint32),
_CryptoRandomBytesDart>('crypto_random_bytes'),
@ -1516,6 +1538,7 @@ class VeilidFFI extends Veilid {
final _CryptoCachedDHDart _cryptoCachedDH;
final _CryptoComputeDHDart _cryptoComputeDH;
final _CryptoGenerateSharedSecretDart _cryptoGenerateSharedSecret;
final _CryptoRandomBytesDart _cryptoRandomBytes;
final _CryptoDefaultSaltLengthDart _cryptoDefaultSaltLength;

View File

@ -271,6 +271,16 @@ class VeilidCryptoSystemJS extends VeilidCryptoSystem {
wasm,
'crypto_compute_dh',
[_kind, jsonEncode(key), jsonEncode(secret)]))));
@override
Future<SharedSecret> generateSharedSecret(
PublicKey key, SecretKey secret, Uint8List domain) async =>
SharedSecret.fromJson(jsonDecode(await _wrapApiPromise(js_util.callMethod(
wasm, 'crypto_generate_shared_secret', [
_kind,
jsonEncode(key),
jsonEncode(secret),
base64UrlNoPadEncode(domain)
]))));
@override
Future<Uint8List> randomBytes(int len) async =>

View File

@ -1186,6 +1186,39 @@ pub extern "C" fn crypto_compute_dh(port: i64, kind: u32, key: FfiStr, secret: F
});
}
#[no_mangle]
pub extern "C" fn crypto_generate_shared_secret(
port: i64,
kind: u32,
key: FfiStr,
secret: FfiStr,
domain: FfiStr,
) {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from(kind);
let key: veilid_core::PublicKey =
veilid_core::deserialize_opt_json(key.into_opt_string()).unwrap();
let secret: veilid_core::SecretKey =
veilid_core::deserialize_opt_json(secret.into_opt_string()).unwrap();
let domain: Vec<u8> = data_encoding::BASE64URL_NOPAD
.decode(domain.into_opt_string().unwrap().as_bytes())
.unwrap();
DartIsolateWrapper::new(port).spawn_result_json(async move {
let veilid_api = get_veilid_api().await?;
let crypto = veilid_api.crypto()?;
let csv = crypto.get(kind).ok_or_else(|| {
veilid_core::VeilidAPIError::invalid_argument(
"crypto_generate_shared_secret",
"kind",
kind.to_string(),
)
})?;
let out = csv.generate_shared_secret(&key, &secret, &domain)?;
APIResult::Ok(out)
});
}
#[no_mangle]
pub extern "C" fn crypto_random_bytes(port: i64, kind: u32, len: u32) {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from(kind);

View File

@ -44,3 +44,26 @@ async def test_hash_and_verify_password(api_connection: veilid.VeilidAPI):
# Password mismatch
phash2 = await cs.hash_password(b"abc1234", salt)
assert not await cs.verify_password(b"abc12345", phash)
@pytest.mark.asyncio
async def test_generate_shared_secret(api_connection: veilid.VeilidAPI):
cs = await api_connection.best_crypto_system()
async with cs:
kp1 = await cs.generate_key_pair()
kp2 = await cs.generate_key_pair()
kp3 = await cs.generate_key_pair()
ssA = await cs.generate_shared_secret(kp1.key(), kp2.secret(), b"abc123")
ssB = await cs.generate_shared_secret(kp2.key(), kp1.secret(), b"abc123")
assert ssA == ssB
ssC = await cs.generate_shared_secret(kp2.key(), kp1.secret(), b"abc1234")
assert ssA != ssC
ssD = await cs.generate_shared_secret(kp3.key(), kp1.secret(), b"abc123")
assert ssA != ssD

View File

@ -65,7 +65,9 @@ async def test_routing_context_app_message_loopback():
await api.debug("purge routes")
# make a routing context that uses a safety route
rc = await api.new_routing_context()
rc = await (await api.new_routing_context()).with_sequencing(
veilid.Sequencing.ENSURE_ORDERED
)
async with rc:
# make a new local private route
prl, blob = await api.new_private_route()
@ -113,7 +115,9 @@ async def test_routing_context_app_call_loopback():
await api.debug("purge routes")
# make a routing context
rc = await api.new_routing_context()
rc = await (await api.new_routing_context()).with_sequencing(
veilid.Sequencing.ENSURE_ORDERED
)
async with rc:
# make a new local private route
prl, blob = await api.new_private_route()
@ -174,7 +178,9 @@ async def test_routing_context_app_message_loopback_big_packets():
await api.debug("purge routes")
# make a routing context that uses a safety route
rc = await api.new_routing_context()
rc = await (await api.new_routing_context()).with_sequencing(
veilid.Sequencing.ENSURE_ORDERED
)
async with rc:
# make a new local private route
prl, blob = await api.new_private_route()
@ -291,7 +297,9 @@ async def test_routing_context_app_message_loopback_bandwidth():
await api.debug("purge routes")
# make a routing context that uses a safety route
rc = await api.new_routing_context()
rc = await (await api.new_routing_context()).with_sequencing(
veilid.Sequencing.ENSURE_ORDERED
)
async with rc:
# make a new local private route
prl, blob = await api.new_private_route()

View File

@ -204,6 +204,12 @@ class CryptoSystem(ABC):
) -> types.SharedSecret:
pass
@abstractmethod
async def generate_shared_secret(
self, key: types.PublicKey, secret: types.SecretKey, domain: bytes
) -> types.SharedSecret:
pass
@abstractmethod
async def random_bytes(self, len: int) -> bytes:
pass

View File

@ -977,6 +977,21 @@ class _JsonCryptoSystem(CryptoSystem):
)
)
async def generate_shared_secret(self, key: PublicKey, secret: SecretKey, domain: bytes) -> SharedSecret:
return SharedSecret(
raise_api_result(
await self.api.send_ndjson_request(
Operation.CRYPTO_SYSTEM,
validate=validate_cs_op,
cs_id=self.cs_id,
cs_op=CryptoSystemOperation.GENERATE_SHARED_SECRET,
key=key,
secret=secret,
domain=domain,
)
)
)
async def random_bytes(self, len: int) -> bytes:
return urlsafe_b64decode_no_pad(
raise_api_result(

View File

@ -75,6 +75,7 @@ class CryptoSystemOperation(StrEnum):
RELEASE = "Release"
CACHED_DH = "CachedDh"
COMPUTE_DH = "ComputeDh"
GENERATE_SHARED_SECRET = "GenerateSharedSecret"
RANDOM_BYTES = "RandomBytes"
DEFAULT_SALT_LENGTH = "DefaultSaltLength"
HASH_PASSWORD = "HashPassword"

View File

@ -1675,6 +1675,44 @@
}
}
},
{
"type": "object",
"anyOf": [
{
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"$ref": "#/definitions/VeilidAPIError"
}
}
}
],
"required": [
"cs_op"
],
"properties": {
"cs_op": {
"type": "string",
"enum": [
"GenerateSharedSecret"
]
}
}
},
{
"type": "object",
"required": [

View File

@ -1007,6 +1007,32 @@
}
}
},
{
"type": "object",
"required": [
"cs_op",
"domain",
"key",
"secret"
],
"properties": {
"cs_op": {
"type": "string",
"enum": [
"GenerateSharedSecret"
]
},
"domain": {
"type": "string"
},
"key": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
{
"type": "object",
"required": [

View File

@ -1048,6 +1048,36 @@ pub fn crypto_compute_dh(kind: u32, key: String, secret: String) -> Promise {
})
}
#[wasm_bindgen()]
pub fn crypto_generate_shared_secret(
kind: u32,
key: String,
secret: String,
domain: String,
) -> Promise {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from(kind);
let key: veilid_core::PublicKey = veilid_core::deserialize_json(&key).unwrap();
let secret: veilid_core::SecretKey = veilid_core::deserialize_json(&secret).unwrap();
let domain: Vec<u8> = data_encoding::BASE64URL_NOPAD
.decode(domain.as_bytes())
.unwrap();
wrap_api_future_json(async move {
let veilid_api = get_veilid_api()?;
let crypto = veilid_api.crypto()?;
let csv = crypto.get(kind).ok_or_else(|| {
veilid_core::VeilidAPIError::invalid_argument(
"crypto_generate_shared_secret",
"kind",
kind.to_string(),
)
})?;
let out = csv.generate_shared_secret(&key, &secret, &domain)?;
APIResult::Ok(out)
})
}
#[wasm_bindgen()]
pub fn crypto_random_bytes(kind: u32, len: u32) -> Promise {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from(kind);

View File

@ -58,6 +58,30 @@ impl VeilidCrypto {
APIResult::Ok(out.to_string())
}
pub fn generateSharedSecret(
kind: String,
key: String,
secret: String,
domain: Box<[u8]>,
) -> APIResult<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?;
let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?;
let veilid_api = get_veilid_api()?;
let crypto = veilid_api.crypto()?;
let crypto_system = crypto.get(kind).ok_or_else(|| {
veilid_core::VeilidAPIError::invalid_argument(
"crypto_generate_shared_secret",
"kind",
kind.to_string(),
)
})?;
let out = crypto_system.generate_shared_secret(&key, &secret, &domain)?;
APIResult::Ok(out.to_string())
}
pub fn randomBytes(kind: String, len: u32) -> APIResult<Box<[u8]>> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;