mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
Kx v1 (#16326)
* key exchange version 1 on the rendezvous and peer handshakes Bump hbb_common for key exchange version 1, one stream key per direction, and negotiate it on both handshakes. Rendezvous: `secure_tcp` reads the version the server advertises in `KeyExchange.version`, answers with the highest both speak, splits the key when that is at least 1, and arms the check of the server's echo on its first encrypted message. Peer: the controlled side advertises `KX_VERSION_LATEST` inside the signed `IdPk`, the controller answers in `PublicKey.kx_version` and both split the key when the pick is at least 1. A pick above what was offered is refused as a bug or tampering. `decode_id_pk_dtls` returns the advertised version along with the fingerprint. An absent field on either side is version 0, so any old peer or server keeps today's stream byte for byte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump hbb_common: keep the advertisement check armed until an echo arrives Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * bump hbb_common: drop box_pk_of until something calls it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: say 0 on WebRTC, where no stream key applies A WebRTC stream is encrypted by DTLS and `set_key_split` collapses to `set_key` on it, which sets nothing but `peer_verified`. Both sides still put 1 on the wire, the controlled peer in its signed identity and the controller in its pick, and agreed on a version neither applied. That held only because both fell into the same branch: the day one side splits keys on WebRTC and the other does not, they would agree on 1, derive different keys, and have no version mismatch to point at. The controlled peer now advertises 0 on WebRTC and refuses a pick above what it advertised rather than above the newest it speaks anywhere; the controller picks 0 there. What is on the wire is what runs. Also bumps hbb_common for the echo contract: the server tags every message after the exchange, not the first alone, so dropping one frame at a known position does not rid an attacker of the downgrade check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: verify the server's signed version before trusting it bump hbb_common d9f519e: KeyExchange.signed_version A server that signs its version sets bit 255 of the ephemeral key inside the signed keys[0]. A client that sees that bit, or the field itself, verifies sign("rdkx-ver" || key || version LE) under the server's signing key before picking a version and refuses the exchange otherwise, so a version lowered in the clear is caught at the handshake rather than one frame pair later by the echo. A legacy server sends canonical keys and no field and takes the unchanged path; the echo still checks its version. Tests: version 1 keys match the server both ways; an advertisement lowered in transit is refused at the first echo; legacy, signed-1 and signed-3 servers each exchange application data; nine tamperings of the signed version are refused before the client replies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: comments say what the fields and functions are bump hbb_common e3452ac: the same on the shared side. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: drop the advertisement echo bump hbb_common 5194159: RendezvousMessage.kx_advertised and the check on decrypted frames are gone. signed_version settles the version inside the exchange, so the client arms nothing after it; the stub in the tests stops tagging its frames and the test of a lowered advertisement goes, the signed-version cases covering that refusal before any reply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: verify KxParams, the server's signed structure bump hbb_common e74a188: KeyExchange.signed_params and KxParams. The client parses the signed bytes as KxParams and compares its fields to the key it verified and the version it was shown, rather than matching a fixed byte string, so a field added to KxParams later parses past an older client. The tamper cases now include a payload signed as the old byte string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: require the KxParams signing prefix before parsing bump hbb_common 26582e1: KX_PARAMS_DOMAIN. Without it a signed IdPk, which the server signs with the same key, parsed as a KxParams whose pk was the id and whose version was 0, so an id registered with the bytes of a marked ephemeral key could stand in for the params at version 0. Two tamper cases pin this: params signed without the prefix, and a signed IdPk in their place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: one call sets the negotiated key bump hbb_common cbc8772: Stream::set_negotiated_key. The rendezvous client, the controller and the controlled side each branched on the picked version to choose between split and single keys. They now hand the transcript to Stream, which makes that choice once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: refuse unknown key exchange versions, pin wire vectors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * client: prove the peer handshake picks its version and encrypts under it The rendezvous exchange had tests against a stub server; the peer handshake had none, so a controller that fell back to version 0 would still connect, still report secured, and pass every test. These run Client::secure_connection against a stub controlled peer and check the pick it sees and the application data both ways: new peers pick 1, a peer without versions gets 0, and a pick lowered in transit leaves the two sides unable to read each other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: pin the subkeys for an advertisement above the pick Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common to main, with #614 merged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: name the picked version, say why the marker bit is safe Review follow-ups with no change in behaviour: the rendezvous pick is called picked, as in the peer handshake and KxTranscript; the marker comment says X25519 ignores the bit and the bytes stay as signed; the controlled side's refusal names the version it offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b30f781fd6
commit
e424b57fca
5 files changed
+497
-17
No files matched your search
+304
-11
@@ -2151,17 +2151,46 @@ async fn key_exchange(conn: &mut Stream, key: &str, log_on_success: bool) -> Res
|
||||
}
|
||||
let their_pk_b = sign::verify(&ex.keys[0], &rs_pk)
|
||||
.map_err(|_| anyhow!("Signature mismatch in key exchange"))?;
|
||||
let (asymmetric_value, symmetric_value, key) = create_symmetric_key_msg(
|
||||
get_pk(&their_pk_b)
|
||||
.context("Wrong their public length in key exchange")?,
|
||||
);
|
||||
let their_pk_b = get_pk(&their_pk_b)
|
||||
.context("Wrong their public length in key exchange")?;
|
||||
// The signed X25519 high bit marks servers that sign their parameters.
|
||||
// X25519 ignores that bit, so old clients use the key unchanged; keep
|
||||
// the bytes as signed, since the transcript and KxParams carry them so.
|
||||
if their_pk_b[31] & 0x80 != 0 || !ex.signed_params.is_empty() {
|
||||
let params = sign::verify(&ex.signed_params, &rs_pk)
|
||||
.ok()
|
||||
.and_then(|signed| {
|
||||
let params =
|
||||
signed.strip_prefix(hbb_common::tcp::KX_PARAMS_DOMAIN)?;
|
||||
KxParams::parse_from_bytes(params).ok()
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing or invalid signed key exchange parameters")
|
||||
})?;
|
||||
if params.pk[..] != their_pk_b[..] || params.version != ex.version {
|
||||
bail!("Key exchange version or public key does not match its signature");
|
||||
}
|
||||
}
|
||||
let (asymmetric_value, symmetric_value, key) =
|
||||
create_symmetric_key_msg(their_pk_b);
|
||||
let picked = hbb_common::tcp::kx_version_for(ex.version);
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_key_exchange(KeyExchange {
|
||||
keys: vec![asymmetric_value, symmetric_value],
|
||||
keys: vec![asymmetric_value.clone(), symmetric_value],
|
||||
version: picked,
|
||||
..Default::default()
|
||||
});
|
||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||
conn.set_key(key);
|
||||
conn.set_negotiated_key(
|
||||
key,
|
||||
true,
|
||||
&hbb_common::tcp::KxTranscript {
|
||||
initiator_pk: &asymmetric_value,
|
||||
responder_pk: &their_pk_b,
|
||||
advertised: ex.version,
|
||||
picked,
|
||||
},
|
||||
)?;
|
||||
if log_on_success {
|
||||
log::info!("Connection secured");
|
||||
}
|
||||
@@ -2221,21 +2250,23 @@ pub fn get_rs_pk(str_base64: &str) -> Option<sign::PublicKey> {
|
||||
}
|
||||
|
||||
pub fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8; 32])> {
|
||||
let (id, pk, _) = decode_id_pk_dtls(signed, key)?;
|
||||
let (id, pk, _, _) = decode_id_pk_dtls(signed, key)?;
|
||||
Ok((id, pk))
|
||||
}
|
||||
|
||||
/// Like [`decode_id_pk`] but also returns the signed DTLS certificate fingerprint (empty string
|
||||
/// for non-WebRTC peers), used to bind a WebRTC DTLS channel to the verified peer identity.
|
||||
/// for non-WebRTC peers), used to bind a WebRTC DTLS channel to the verified peer identity, and
|
||||
/// the newest key exchange version the peer speaks (0, the original scheme, for a peer from
|
||||
/// before versions).
|
||||
pub fn decode_id_pk_dtls(
|
||||
signed: &[u8],
|
||||
key: &sign::PublicKey,
|
||||
) -> ResultType<(String, [u8; 32], String)> {
|
||||
) -> ResultType<(String, [u8; 32], String, u32)> {
|
||||
let res = IdPk::parse_from_bytes(
|
||||
&sign::verify(signed, key).map_err(|_| anyhow!("Signature mismatch"))?,
|
||||
)?;
|
||||
if let Some(pk) = get_pk(&res.pk) {
|
||||
Ok((res.id, pk, res.dtls_fingerprint))
|
||||
Ok((res.id, pk, res.dtls_fingerprint, res.kx_version))
|
||||
} else {
|
||||
bail!("Wrong their public length");
|
||||
}
|
||||
@@ -3384,6 +3415,26 @@ mod tests {
|
||||
(encode64(pk.0), sk)
|
||||
}
|
||||
|
||||
fn signed_key_exchange(
|
||||
pk: &box_::PublicKey,
|
||||
sk: &sign::SecretKey,
|
||||
version: u32,
|
||||
) -> KeyExchange {
|
||||
let params = KxParams {
|
||||
pk: pk.0.to_vec().into(),
|
||||
version,
|
||||
..Default::default()
|
||||
};
|
||||
let mut payload = hbb_common::tcp::KX_PARAMS_DOMAIN.to_vec();
|
||||
payload.extend_from_slice(¶ms.write_to_bytes().unwrap());
|
||||
KeyExchange {
|
||||
keys: vec![sign::sign(&pk.0, sk).into()],
|
||||
version,
|
||||
signed_params: sign::sign(&payload, sk).into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(host: &str) -> Stream {
|
||||
hbb_common::socket_client::connect_tcp(host.to_owned(), 3000)
|
||||
.await
|
||||
@@ -3445,6 +3496,248 @@ mod tests {
|
||||
assert!(conn.is_secured());
|
||||
}
|
||||
|
||||
// The stand-in does what hbbs does at version 1: advertises it and splits the exchanged key
|
||||
// over the same transcript. Neither side's unit tests can catch a client that puts a
|
||||
// different byte string into the transcript than the server does; only a frame crossing
|
||||
// between the two can.
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_version_1_keys_match_the_server_both_ways() {
|
||||
let (key, sk) = server_key();
|
||||
let host = rendezvous_stub(move |mut s| async move {
|
||||
let (eph_pk, eph_sk) = box_::gen_keypair();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_key_exchange(KeyExchange {
|
||||
keys: vec![sign::sign(&eph_pk.0, &sk).into()],
|
||||
version: 1,
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
let reply = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::KeyExchange(ex)) = reply.union else {
|
||||
panic!("expected the client's key exchange");
|
||||
};
|
||||
let shared =
|
||||
hbb_common::tcp::Encrypt::decode(&ex.keys[1], &ex.keys[0], &eph_sk).unwrap();
|
||||
s.set_key_split(
|
||||
shared,
|
||||
false,
|
||||
&hbb_common::tcp::KxTranscript {
|
||||
initiator_pk: &ex.keys[0],
|
||||
responder_pk: &eph_pk.0,
|
||||
advertised: 1,
|
||||
picked: ex.version,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// Answers with the request's serial, so the client learns from its own reply that
|
||||
// the request was read under the right key.
|
||||
let request = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let request = RendezvousMessage::parse_from_bytes(&request).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatRequest(nat)) = request.union else {
|
||||
panic!("expected the client's nat request");
|
||||
};
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_response(TestNatResponse {
|
||||
port: nat.serial,
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
})
|
||||
.await;
|
||||
let mut conn = connect(&host).await;
|
||||
secure_tcp_required(&mut conn, &key).await.unwrap();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_request(TestNatRequest {
|
||||
serial: 7,
|
||||
..Default::default()
|
||||
});
|
||||
conn.send(&msg).await.unwrap();
|
||||
let reply = conn.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatResponse(nat)) = reply.union else {
|
||||
panic!("expected the server's nat response");
|
||||
};
|
||||
assert_eq!(nat.port, 7);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_legacy_and_signed_servers_exchange_application_data() {
|
||||
for (advertised, signed) in [(0, false), (1, true), (3, true)] {
|
||||
for required in [false, true] {
|
||||
let (key, sk) = server_key();
|
||||
let host = rendezvous_stub(move |mut s| async move {
|
||||
let (mut eph_pk, eph_sk) = box_::gen_keypair();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
let ex = if signed {
|
||||
eph_pk.0[31] |= 0x80;
|
||||
signed_key_exchange(&eph_pk, &sk, advertised)
|
||||
} else {
|
||||
KeyExchange {
|
||||
keys: vec![sign::sign(&eph_pk.0, &sk).into()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
msg.set_key_exchange(ex);
|
||||
s.send(&msg).await.unwrap();
|
||||
let reply = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::KeyExchange(ex)) = reply.union else {
|
||||
panic!("expected the client's key exchange");
|
||||
};
|
||||
assert_eq!(ex.keys.len(), 2);
|
||||
assert_eq!(ex.version, hbb_common::tcp::kx_version_for(advertised));
|
||||
let shared =
|
||||
hbb_common::tcp::Encrypt::decode(&ex.keys[1], &ex.keys[0], &eph_sk)
|
||||
.unwrap();
|
||||
if ex.version >= 1 {
|
||||
s.set_key_split(
|
||||
shared,
|
||||
false,
|
||||
&hbb_common::tcp::KxTranscript {
|
||||
initiator_pk: &ex.keys[0],
|
||||
responder_pk: &eph_pk.0,
|
||||
advertised,
|
||||
picked: ex.version,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
s.set_key(shared);
|
||||
}
|
||||
let request = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let request = RendezvousMessage::parse_from_bytes(&request).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatRequest(nat)) = request.union else {
|
||||
panic!("expected the client's nat request");
|
||||
};
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_response(TestNatResponse {
|
||||
port: nat.serial,
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
})
|
||||
.await;
|
||||
let mut conn = connect(&host).await;
|
||||
if required {
|
||||
secure_tcp_required(&mut conn, &key).await.unwrap();
|
||||
} else {
|
||||
secure_tcp(&mut conn, &key).await.unwrap();
|
||||
}
|
||||
assert!(conn.is_secured());
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_request(TestNatRequest {
|
||||
serial: 7,
|
||||
..Default::default()
|
||||
});
|
||||
conn.send(&msg).await.unwrap();
|
||||
let reply = conn.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatResponse(nat)) = reply.union else {
|
||||
panic!("expected the server's nat response");
|
||||
};
|
||||
assert_eq!(nat.port, 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_rejects_unverified_versions_before_reply() {
|
||||
let (key, sk) = server_key();
|
||||
let (mut eph_pk, _) = box_::gen_keypair();
|
||||
eph_pk.0[31] |= 0x80;
|
||||
let (other_pk, _) = box_::gen_keypair();
|
||||
let (_, other_sk) = sign::gen_keypair();
|
||||
for case in [
|
||||
"missing_signature",
|
||||
"stripped_version_and_signature",
|
||||
"cleared_marker_and_stripped_fields",
|
||||
"lowered_version",
|
||||
"raised_version",
|
||||
"replaced_public_key",
|
||||
"invalid_signature",
|
||||
"wrong_signer",
|
||||
"unstructured_payload",
|
||||
"undomained_params",
|
||||
"signed_id_pk_as_params",
|
||||
] {
|
||||
let mut ex = signed_key_exchange(&eph_pk, &sk, 1);
|
||||
match case {
|
||||
"missing_signature" => ex.signed_params = Bytes::new(),
|
||||
"stripped_version_and_signature" => {
|
||||
ex.signed_params = Bytes::new();
|
||||
ex.version = 0;
|
||||
}
|
||||
"cleared_marker_and_stripped_fields" => {
|
||||
let mut signed_pk = ex.keys[0].to_vec();
|
||||
*signed_pk.last_mut().unwrap() &= 0x7f;
|
||||
ex.keys[0] = signed_pk.into();
|
||||
ex.signed_params = Bytes::new();
|
||||
ex.version = 0;
|
||||
}
|
||||
"lowered_version" => ex.version = 0,
|
||||
"raised_version" => ex.version = 2,
|
||||
"replaced_public_key" => ex.keys[0] = sign::sign(&other_pk.0, &sk).into(),
|
||||
"invalid_signature" => {
|
||||
let mut signed = ex.signed_params.to_vec();
|
||||
signed[0] ^= 1;
|
||||
ex.signed_params = signed.into();
|
||||
}
|
||||
"wrong_signer" => {
|
||||
ex.signed_params = signed_key_exchange(&eph_pk, &other_sk, 1).signed_params;
|
||||
}
|
||||
"unstructured_payload" => {
|
||||
let mut payload = eph_pk.0.to_vec();
|
||||
payload.extend_from_slice(&1u32.to_le_bytes());
|
||||
ex.signed_params = sign::sign(&payload, &sk).into();
|
||||
}
|
||||
"undomained_params" => {
|
||||
let params = KxParams {
|
||||
pk: eph_pk.0.to_vec().into(),
|
||||
version: 1,
|
||||
..Default::default()
|
||||
};
|
||||
ex.signed_params = sign::sign(¶ms.write_to_bytes().unwrap(), &sk).into();
|
||||
}
|
||||
"signed_id_pk_as_params" => {
|
||||
// The server signs IdPk with the same key; its `id` sits where `pk` does.
|
||||
let mut pk = [0x2au8; box_::PUBLICKEYBYTES];
|
||||
pk[30] = 0xc2;
|
||||
pk[31] = 0xaa;
|
||||
let id_pk = IdPk {
|
||||
id: String::from_utf8(pk.to_vec()).unwrap(),
|
||||
pk: vec![7u8; box_::PUBLICKEYBYTES].into(),
|
||||
..Default::default()
|
||||
};
|
||||
ex.keys[0] = sign::sign(&pk, &sk).into();
|
||||
ex.version = 0;
|
||||
ex.signed_params = sign::sign(&id_pk.write_to_bytes().unwrap(), &sk).into();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
for required in [false, true] {
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_key_exchange(ex.clone());
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let host = rendezvous_stub(move |mut s| async move {
|
||||
s.send(&msg).await.unwrap();
|
||||
tx.send(s.next_timeout(3000).await.is_none()).unwrap();
|
||||
})
|
||||
.await;
|
||||
let mut conn = connect(&host).await;
|
||||
let result = if required {
|
||||
secure_tcp_required(&mut conn, &key).await
|
||||
} else {
|
||||
secure_tcp(&mut conn, &key).await
|
||||
};
|
||||
assert!(result.is_err(), "accepted {case}, required={required}");
|
||||
assert!(!conn.is_secured());
|
||||
drop(conn);
|
||||
assert!(rx.await.unwrap(), "replied to {case}, required={required}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dtls_fingerprint_travels_signed_and_binds() {
|
||||
let (pk, sk) = sign::gen_keypair();
|
||||
@@ -3461,7 +3754,7 @@ mod tests {
|
||||
&sk,
|
||||
);
|
||||
|
||||
let (id, their_pk, signed_fp) = decode_id_pk_dtls(&signed, &pk).unwrap();
|
||||
let (id, their_pk, signed_fp, _) = decode_id_pk_dtls(&signed, &pk).unwrap();
|
||||
assert_eq!(id, "123456789");
|
||||
assert_eq!(their_pk, [7u8; 32]);
|
||||
assert_eq!(signed_fp, fp);
|
||||
|
||||
Reference in new issue
Block a user