Sweep the handshake variants, since the pod answers back
The first two candidate runs looked like failures and were not. Buried in
them: a `0x3e` frame arriving 90 ms after our write, in both runs, never
otherwise — `{1: 255, 2: 5}`. The pod parsed what we sent and rejected it
with a reason. That is a feedback channel, and it turns this from
guessing into navigating.
Both candidates drew the *same* reason, so the field-2 marker is not what
it objects to. `--sweep` therefore sends every variant down one
connection and prints the reply to each: field 1 alone, the pod's own
trailer echoed back, an uncompressed 65-byte point, and the documented
2023 Play handshake verbatim (`RideOn 01 02` + a raw 64-byte key, no
protobuf at all) — which we had never actually tried, having assumed the
protobuf shape from the offer.
It needs no button presses. That matters now: this pod has stopped
reporting buttons entirely, so the paddle oracle the rest of the command
depends on is unavailable, and a sweep that reads only the reply code
still works.
Fuzzing a pod is not fuzzing a trainer. §2.3 refused unknown writes to
the D100 because it puts resistance under a rider; a Click has no
actuator and the worst it can do is ignore us. The OAD characteristics
stay untouched — those can brick a sealed unit.
Two corrections to the tool while here. Button frames were counted but
never printed, so an operator pressing into a silent terminal could not
tell a working run from a dead pod and reasonably concluded the latter.
And the cliff is now taken from an actual `flag 0 -> 1` transition rather
than the first sighting of a 1 — these runs opened with the flag already
set, the pod having kept that state across the reconnect, and reporting
"cliff at 2.3s" for it was a reading dressed as a measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -22,7 +22,9 @@ SUBCOMMANDS:
|
|||||||
set <ADDR> <TARGET> Take control and apply a target, then reset the trainer to zero
|
set <ADDR> <TARGET> Take control and apply a target, then reset the trainer to zero
|
||||||
unlock <ADDR> Answer the pod's key offer and see whether its paddles survive
|
unlock <ADDR> Answer the pod's key offer and see whether its paddles survive
|
||||||
past the ~50 s cliff (--candidate ours|play|echo; omit for a
|
past the ~50 s cliff (--candidate ours|play|echo; omit for a
|
||||||
control run that answers nothing)
|
control run that answers nothing). --sweep sends every known
|
||||||
|
variant in one connection and compares the pod's replies —
|
||||||
|
no button presses needed
|
||||||
zwift <ADDR> Talk to Zwift's custom service: handshake, then log every frame
|
zwift <ADDR> Talk to Zwift's custom service: handshake, then log every frame
|
||||||
listen <ADDR> Raw GATT: dump services, characteristics and descriptors, then
|
listen <ADDR> Raw GATT: dump services, characteristics and descriptors, then
|
||||||
subscribe to everything and print each notification's
|
subscribe to everything and print each notification's
|
||||||
@@ -103,6 +105,10 @@ pub enum Command {
|
|||||||
/// Which field-2 marker to answer with; `None` sends nothing and is the
|
/// Which field-2 marker to answer with; `None` sends nothing and is the
|
||||||
/// control run.
|
/// control run.
|
||||||
candidate: Option<String>,
|
candidate: Option<String>,
|
||||||
|
/// Send every variant in one connection and compare the pod's replies.
|
||||||
|
/// Needs no button presses, so it works on a pod that has stopped
|
||||||
|
/// reporting them.
|
||||||
|
sweep: bool,
|
||||||
},
|
},
|
||||||
/// Phase 3 / TASK-0: exercise Zwift's custom service on whatever advertises
|
/// Phase 3 / TASK-0: exercise Zwift's custom service on whatever advertises
|
||||||
/// it — a Click, or the trainer itself.
|
/// it — a Click, or the trainer itself.
|
||||||
@@ -140,6 +146,7 @@ pub fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args> {
|
|||||||
let mut buttons_only = false;
|
let mut buttons_only = false;
|
||||||
let mut name: Option<String> = None;
|
let mut name: Option<String> = None;
|
||||||
let mut candidate: Option<String> = None;
|
let mut candidate: Option<String> = None;
|
||||||
|
let mut sweep = false;
|
||||||
let mut help = false;
|
let mut help = false;
|
||||||
let mut positional: Vec<String> = Vec::new();
|
let mut positional: Vec<String> = Vec::new();
|
||||||
|
|
||||||
@@ -163,6 +170,7 @@ pub fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args> {
|
|||||||
.map_err(|_| anyhow!("--secs expects a whole number of seconds, got {v:?}"))?,
|
.map_err(|_| anyhow!("--secs expects a whole number of seconds, got {v:?}"))?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
"--sweep" => sweep = true,
|
||||||
"--candidate" => {
|
"--candidate" => {
|
||||||
i += 1;
|
i += 1;
|
||||||
candidate = Some(
|
candidate = Some(
|
||||||
@@ -244,6 +252,7 @@ pub fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args> {
|
|||||||
// merely delays the failure must not read as one that fixed it.
|
// merely delays the failure must not read as one that fixed it.
|
||||||
duration: Duration::from_secs(secs.unwrap_or(150)),
|
duration: Duration::from_secs(secs.unwrap_or(150)),
|
||||||
candidate: candidate.clone(),
|
candidate: candidate.clone(),
|
||||||
|
sweep,
|
||||||
},
|
},
|
||||||
"zwift" => Command::Zwift {
|
"zwift" => Command::Zwift {
|
||||||
device: device(&positional, 1)?,
|
device: device(&positional, 1)?,
|
||||||
|
|||||||
@@ -724,6 +724,7 @@ pub async fn zwift_cmd(
|
|||||||
// Only meaningful in --buttons mode: the mask as of the previous frame, so
|
// Only meaningful in --buttons mode: the mask as of the previous frame, so
|
||||||
// the ~10 Hz repeat while a button is held collapses to one line.
|
// the ~10 Hz repeat while a button is held collapses to one line.
|
||||||
let mut last_mask: Option<u32> = None;
|
let mut last_mask: Option<u32> = None;
|
||||||
|
|
||||||
let mut presses: u64 = 0;
|
let mut presses: u64 = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -974,6 +975,7 @@ pub async fn unlock_cmd(
|
|||||||
device: &Device,
|
device: &Device,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
candidate: Option<&str>,
|
candidate: Option<&str>,
|
||||||
|
sweep: bool,
|
||||||
scan_timeout: Duration,
|
scan_timeout: Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use crate::unlock;
|
use crate::unlock;
|
||||||
@@ -1038,6 +1040,14 @@ pub async fn unlock_cmd(
|
|||||||
let mut answered = 0u32;
|
let mut answered = 0u32;
|
||||||
let mut offers = 0u32;
|
let mut offers = 0u32;
|
||||||
let mut last_status: Option<unlock::Status> = None;
|
let mut last_status: Option<unlock::Status> = None;
|
||||||
|
// When the pod flipped its status flag, which is the cliff this run is
|
||||||
|
// measured against — better than a constant, because the pod says so.
|
||||||
|
let mut sent: Vec<&'static str> = Vec::new();
|
||||||
|
let mut responses: Vec<(&'static str, unlock::Response)> = Vec::new();
|
||||||
|
let mut last_mask: Option<u32> = None;
|
||||||
|
// When the pod flipped its status flag, which is the cliff this run is
|
||||||
|
// measured against — better than a constant, because the pod says so.
|
||||||
|
let mut flip_at: Option<Duration> = None;
|
||||||
|
|
||||||
let deadline = tokio::time::sleep(duration);
|
let deadline = tokio::time::sleep(duration);
|
||||||
tokio::pin!(deadline);
|
tokio::pin!(deadline);
|
||||||
@@ -1074,7 +1084,24 @@ pub async fn unlock_cmd(
|
|||||||
),
|
),
|
||||||
Err(e) => println!(" !! {e}"),
|
Err(e) => println!(" !! {e}"),
|
||||||
}
|
}
|
||||||
if let Some(c) = candidate {
|
if sweep {
|
||||||
|
// One connection, every variant, because the pod hands
|
||||||
|
// back a reason for each. Spaced so a late reply cannot
|
||||||
|
// be attributed to the next thing we sent.
|
||||||
|
for v in unlock::VARIANTS {
|
||||||
|
let frame = (v.build)(&local, &offer);
|
||||||
|
println!("\n -> {:<20} {}", v.name, hex(&frame));
|
||||||
|
println!(" {}", v.why);
|
||||||
|
if let Err(e) = write_frame(&peripheral, &sync_rx, &frame).await {
|
||||||
|
println!(" !! could not send: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
answered += 1;
|
||||||
|
sent.push(v.name);
|
||||||
|
tokio::time::sleep(Duration::from_millis(1200)).await;
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
} else if let Some(c) = candidate {
|
||||||
let frame = unlock::reply_frame(&local.compressed, c.marker);
|
let frame = unlock::reply_frame(&local.compressed, c.marker);
|
||||||
match write_frame(&peripheral, &sync_rx, &frame).await {
|
match write_frame(&peripheral, &sync_rx, &frame).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -1093,20 +1120,75 @@ pub async fn unlock_cmd(
|
|||||||
"[{at:7.2}s] STATUS flag={} timer={}",
|
"[{at:7.2}s] STATUS flag={} timer={}",
|
||||||
status.flag, status.timer
|
status.flag, status.timer
|
||||||
);
|
);
|
||||||
|
// The pod telling us, in its own words, that whatever
|
||||||
|
// grace it was extending has ended. Everything before
|
||||||
|
// this is preamble; the run is only evidence from here.
|
||||||
|
// A *transition*, not merely a first sighting. These
|
||||||
|
// runs opened with flag already 1 — the pod remembers
|
||||||
|
// being past the cliff across reconnects — and calling
|
||||||
|
// that "the cliff at 2.3s" is a reading, not a fact.
|
||||||
|
let was_zero = last_status.is_some_and(|s| s.flag == 0);
|
||||||
|
if status.flag == 1 && was_zero && flip_at.is_none() {
|
||||||
|
flip_at = Some(start.elapsed());
|
||||||
|
println!(
|
||||||
|
"\n >>> THE CLIFF. Keep pressing both paddles and the D-pad for\n >>> another 60 s — everything before this line proves nothing.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
last_status = Some(status);
|
last_status = Some(status);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(r) = unlock::parse_response(&n.value) {
|
||||||
|
let to = sent.last().copied().unwrap_or("(unsolicited)");
|
||||||
|
println!("[{at:7.2}s] REPLY code={} detail={} <- {to}", r.code, r.detail);
|
||||||
|
responses.push((to, r));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_mask(&n.value).is_none()
|
||||||
|
&& unlock::parse_key_offer(&n.value).is_none()
|
||||||
|
&& unlock::parse_status(&n.value).is_none()
|
||||||
|
&& unlock::parse_response(&n.value).is_none()
|
||||||
|
{
|
||||||
|
println!("[{at:7.2}s] other {}", hex(&n.value));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(mask) = button_mask(&n.value) {
|
if let Some(mask) = button_mask(&n.value) {
|
||||||
|
// Printed, not merely counted. An operator pressing buttons
|
||||||
|
// into a silent terminal cannot tell a working run from a
|
||||||
|
// dead pod, and will reasonably conclude the latter.
|
||||||
|
if last_mask != Some(mask.raw) {
|
||||||
|
last_mask = Some(mask.raw);
|
||||||
|
let paddles = mask.raw & ((1 << 8) | (1 << 12));
|
||||||
|
let which = match paddles {
|
||||||
|
p if p == (1 << 8) | (1 << 12) => "",
|
||||||
|
p if p & (1 << 8) == 0 => " <- − PADDLE",
|
||||||
|
_ => " <- + PADDLE",
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"[{at:7.2}s] buttons 0x{:08x}{}{}",
|
||||||
|
mask.raw,
|
||||||
|
if mask.is_idle() { " (idle)" } else { "" },
|
||||||
|
which
|
||||||
|
);
|
||||||
|
}
|
||||||
verdict.observe(mask.raw);
|
verdict.observe(mask.raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cliff = Duration::from_secs(60);
|
// The pod's own flip where we saw it; otherwise the ~50 s the captures show.
|
||||||
|
let cliff = flip_at.unwrap_or(Duration::from_secs(50));
|
||||||
println!("\n=== verdict ===");
|
println!("\n=== verdict ===");
|
||||||
|
match (flip_at, last_status) {
|
||||||
|
(Some(t), _) => println!(" cliff (flag 0->1): {:.1}s", t.as_secs_f32()),
|
||||||
|
(None, Some(s)) if s.flag == 1 => println!(
|
||||||
|
" cliff: already past it when we connected — the pod kept\n \x20 that state across the reconnect"
|
||||||
|
),
|
||||||
|
_ => println!(" cliff: never flipped"),
|
||||||
|
}
|
||||||
println!(" key offers seen: {offers}");
|
println!(" key offers seen: {offers}");
|
||||||
println!(" answered: {answered}");
|
println!(" answered: {answered}");
|
||||||
println!(
|
println!(
|
||||||
@@ -1120,6 +1202,28 @@ pub async fn unlock_cmd(
|
|||||||
verdict.last_other.map_or("never".into(), |t| format!("{:.1}s", t.as_secs_f32()))
|
verdict.last_other.map_or("never".into(), |t| format!("{:.1}s", t.as_secs_f32()))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if sweep {
|
||||||
|
println!("\n variant reply");
|
||||||
|
for (name, r) in &responses {
|
||||||
|
println!(" {name:<22} code={} detail={}", r.code, r.detail);
|
||||||
|
}
|
||||||
|
let distinct: std::collections::BTreeSet<_> =
|
||||||
|
responses.iter().map(|(_, r)| (r.code, r.detail)).collect();
|
||||||
|
if responses.is_empty() {
|
||||||
|
println!("\n The pod answered none of them, which is itself a change from\n the runs where it answered `ff 03 00` frames.");
|
||||||
|
} else if distinct.len() == 1 {
|
||||||
|
println!(
|
||||||
|
"\n Every variant drew the same reply, so none of the things varied —\n the marker, the trailer, the key encoding, the envelope — is what\n it is objecting to."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"\n The reply MOVED. Whichever variant differs is the thread to pull:\n that is the first time this device has told us we got warmer."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
disconnect(&peripheral).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if verdict.paddle_edges == 0 && verdict.other_edges == 0 {
|
if verdict.paddle_edges == 0 && verdict.other_edges == 0 {
|
||||||
println!(
|
println!(
|
||||||
"\n INCONCLUSIVE — no buttons at all. Press things during the run;\n\
|
"\n INCONCLUSIVE — no buttons at all. Press things during the run;\n\
|
||||||
@@ -1132,12 +1236,16 @@ pub async fn unlock_cmd(
|
|||||||
);
|
);
|
||||||
} else if verdict.last_paddle.is_some_and(|t| t > cliff) {
|
} else if verdict.last_paddle.is_some_and(|t| t > cliff) {
|
||||||
println!(
|
println!(
|
||||||
"\n HELD — a paddle edge arrived after {}s, past the cliff.\n\
|
"\n HELD — a paddle edge arrived {:.1}s past the cliff.\n\
|
||||||
Worth repeating before believing: run it again, and run the control.",
|
Worth repeating before believing: run it again, and run the control.",
|
||||||
cliff.as_secs()
|
(verdict.last_paddle.unwrap() - cliff).as_secs_f32()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("\n INCONCLUSIVE — the run ended before the cliff, or the paddles were idle.");
|
println!(
|
||||||
|
"\n INCONCLUSIVE — nothing was pressed after the cliff at {:.1}s.\n\
|
||||||
|
The run has to keep going, with fingers on the buttons, well past it.",
|
||||||
|
cliff.as_secs_f32()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(&peripheral).await;
|
disconnect(&peripheral).await;
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ async fn main() -> Result<()> {
|
|||||||
device,
|
device,
|
||||||
duration,
|
duration,
|
||||||
candidate,
|
candidate,
|
||||||
} => commands::unlock_cmd(&device, duration, candidate.as_deref(), SCAN_TIMEOUT).await,
|
sweep,
|
||||||
|
} => {
|
||||||
|
commands::unlock_cmd(&device, duration, candidate.as_deref(), sweep, SCAN_TIMEOUT).await
|
||||||
|
}
|
||||||
cli::Command::Zwift {
|
cli::Command::Zwift {
|
||||||
device,
|
device,
|
||||||
duration,
|
duration,
|
||||||
|
|||||||
+112
-6
@@ -66,6 +66,108 @@ pub const CANDIDATES: &[Candidate] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// One thing to send, and what it is testing.
|
||||||
|
///
|
||||||
|
/// The sweep exists because the pod **answers**: a `0x3e` frame came back
|
||||||
|
/// 90 ms after our first write, in both runs, carrying `{1: 255, 2: 5}`. That
|
||||||
|
/// is a rejection with a reason, which makes this a conversation rather than a
|
||||||
|
/// guess — vary one thing, watch the reason move.
|
||||||
|
///
|
||||||
|
/// It needs no button presses, which matters: the pod this was written for has
|
||||||
|
/// stopped reporting buttons entirely, and the paddle oracle is unavailable
|
||||||
|
/// until it recovers.
|
||||||
|
pub struct Variant {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub why: &'static str,
|
||||||
|
/// Built from our public key and, where it matters, the pod's own offer.
|
||||||
|
pub build: fn(&LocalKey, &KeyOffer) -> Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fuzzing a Click is not fuzzing a trainer. §2.3 refused to fuzz unknown
|
||||||
|
/// writes to the D100 because it puts resistance under a rider; a pod has no
|
||||||
|
/// actuator, and the worst it can do is ignore us. The OAD characteristics stay
|
||||||
|
/// untouched — those *can* brick it, and it is sealed.
|
||||||
|
pub const VARIANTS: &[Variant] = &[
|
||||||
|
Variant {
|
||||||
|
name: "compressed+ours",
|
||||||
|
why: "what we have already sent twice — the control for the sweep",
|
||||||
|
build: |k, _| reply_frame(&k.compressed, 0x0009_0000),
|
||||||
|
},
|
||||||
|
Variant {
|
||||||
|
name: "compressed+none",
|
||||||
|
why: "field 1 alone, in case field 2 is the objection",
|
||||||
|
build: |k, _| {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
field_bytes(&mut body, 1, &k.compressed);
|
||||||
|
envelope(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Variant {
|
||||||
|
name: "compressed+trailer",
|
||||||
|
why: "the pod's own field 3 echoed back — if the trailer is load-bearing, this is the cheapest way to find out",
|
||||||
|
build: |k, offer| {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
field_bytes(&mut body, 1, &k.compressed);
|
||||||
|
field_varint(&mut body, 2, 0x0009_0000);
|
||||||
|
field_bytes(&mut body, 3, &offer.trailer);
|
||||||
|
envelope(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Variant {
|
||||||
|
name: "uncompressed+ours",
|
||||||
|
why: "a 65-byte SEC1 point, since the Play generation exchanged uncompressed keys",
|
||||||
|
build: |k, _| reply_frame(&k.uncompressed, 0x0009_0000),
|
||||||
|
},
|
||||||
|
Variant {
|
||||||
|
name: "play-rideon",
|
||||||
|
why: "the documented 2023 Play handshake verbatim: RideOn 01 02 + a raw 64-byte key, no protobuf envelope at all",
|
||||||
|
build: |k, _| {
|
||||||
|
let mut frame = Vec::with_capacity(72);
|
||||||
|
frame.extend_from_slice(b"RideOn");
|
||||||
|
frame.extend_from_slice(&[0x01, 0x02]);
|
||||||
|
// SEC1 uncompressed minus the 0x04 tag, which is how Play carried it.
|
||||||
|
frame.extend_from_slice(&k.uncompressed[1..]);
|
||||||
|
frame
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// `ff 03 00` around a protobuf body.
|
||||||
|
fn envelope(body: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut frame = Vec::with_capacity(body.len() + 3);
|
||||||
|
frame.extend_from_slice(&[0xff, 0x03, 0x00]);
|
||||||
|
frame.extend_from_slice(&body);
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pod's answer to something we sent. `0x3e`, two varints.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Response {
|
||||||
|
pub code: u64,
|
||||||
|
pub detail: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_response(raw: &[u8]) -> Option<Response> {
|
||||||
|
if raw.first() != Some(&0x3e) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut i = 1;
|
||||||
|
let mut r = Response { code: 0, detail: 0 };
|
||||||
|
while i < raw.len() {
|
||||||
|
let tag = read_varint(raw, &mut i)?;
|
||||||
|
let v = match tag & 7 {
|
||||||
|
0 => read_varint(raw, &mut i)?,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
match tag >> 3 {
|
||||||
|
1 => r.code = v,
|
||||||
|
2 => r.detail = v,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn candidate(name: &str) -> Option<Candidate> {
|
pub fn candidate(name: &str) -> Option<Candidate> {
|
||||||
CANDIDATES.iter().find(|c| c.name == name).copied()
|
CANDIDATES.iter().find(|c| c.name == name).copied()
|
||||||
}
|
}
|
||||||
@@ -293,17 +395,21 @@ impl Verdict {
|
|||||||
pub struct LocalKey {
|
pub struct LocalKey {
|
||||||
pub secret: p256::ecdh::EphemeralSecret,
|
pub secret: p256::ecdh::EphemeralSecret,
|
||||||
pub compressed: Vec<u8>,
|
pub compressed: Vec<u8>,
|
||||||
|
/// SEC1 uncompressed, `04 ‖ X ‖ Y`, 65 bytes.
|
||||||
|
pub uncompressed: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_key() -> LocalKey {
|
pub fn local_key() -> LocalKey {
|
||||||
use p256::elliptic_curve::sec1::ToEncodedPoint;
|
use p256::elliptic_curve::sec1::ToEncodedPoint;
|
||||||
let secret = p256::ecdh::EphemeralSecret::random(&mut rand_core::OsRng);
|
let secret = p256::ecdh::EphemeralSecret::random(&mut rand_core::OsRng);
|
||||||
let compressed = secret
|
let public = secret.public_key();
|
||||||
.public_key()
|
let compressed = public.to_encoded_point(true).as_bytes().to_vec();
|
||||||
.to_encoded_point(true)
|
let uncompressed = public.to_encoded_point(false).as_bytes().to_vec();
|
||||||
.as_bytes()
|
LocalKey {
|
||||||
.to_vec();
|
secret,
|
||||||
LocalKey { secret, compressed }
|
compressed,
|
||||||
|
uncompressed,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort ECDH against the pod's offered point, for the log. A key we
|
/// Best-effort ECDH against the pod's offered point, for the log. A key we
|
||||||
|
|||||||
Reference in New Issue
Block a user