myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit a2e3f9749773c47b6ae7c8c4a06b29c0aa4e0cf6
parent 77b381648ed1e586efb696888beb05b9215c69cf
Author: triesap <tyson@radroots.org>
Date:   Tue, 25 Aug 2026 02:16:05 +0000

nip46: bind connect to transport identity

- validate the NIP-46 remote signer key against the transport provider
- reject user and unrelated identities before connection admission
- update the accumulated NIP-46 wave fixture to the protocol identity
- preserve the separate user identity for delegated signing operations

Diffstat:
Msrc/nip46_wave_080_a.rs | 2+-
Msrc/nip46_work.rs | 38+++++++++++++++++++++++++++++++++++---
2 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/src/nip46_wave_080_a.rs b/src/nip46_wave_080_a.rs @@ -216,7 +216,7 @@ pub(crate) fn connect_request() -> Request { Request::from_parts( Method::Connect, vec![ - keys(3).public_key().to_hex(), + keys(2).public_key().to_hex(), String::new(), "sign_event:kind:1".to_owned(), ], diff --git a/src/nip46_work.rs b/src/nip46_work.rs @@ -449,8 +449,8 @@ pub fn prepare_myc_nip46_work( if connection.is_some() || provider_deadline.is_some() || secret.is_some() { return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); } - let user = user_binding(providers)?; - if remote_signer_public_key.to_hex() != user.expected_identity().as_hex() { + let transport = transport_binding(providers)?; + if remote_signer_public_key.to_hex() != transport.expected_identity().as_hex() { return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); } let permissions = connect_permissions(requested_permissions.as_slice())?; @@ -607,6 +607,14 @@ fn user_binding(providers: &MycProviderContract) -> Result<&MycProviderBinding, .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding)) } +fn transport_binding( + providers: &MycProviderContract, +) -> Result<&MycProviderBinding, MycNip46WorkError> { + providers + .binding(MycProviderRole::Transport) + .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding)) +} + fn sign_event_input( event: &ConnectUnsignedEvent, providers: &MycProviderContract, @@ -989,7 +997,7 @@ mod tests { let connect = protocol_request( Method::Connect, vec![ - keys(3).public_key().to_hex(), + keys(2).public_key().to_hex(), String::new(), "nip44_encrypt,sign_event:kind:1".into(), ], @@ -1016,6 +1024,30 @@ mod tests { ) .expect("admission request"); + for (request_seed, remote_signer_seed) in [(21, 3), (22, 4)] { + let wrong_identity_connect = protocol_request( + Method::Connect, + vec![ + keys(remote_signer_seed).public_key().to_hex(), + String::new(), + ], + ); + let prepared = prepared_request(wrong_identity_connect, request_seed); + let record = prepared.signer_request.admitted_record_for_test(); + assert_eq!( + prepare_myc_nip46_work( + prepared, + record, + None, + configuration().provider_contract(), + MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), + None, + ) + .expect_err("connect remote signer identity mismatch"), + work_error(MycNip46WorkErrorKind::InvalidBinding) + ); + } + let secret_connect = protocol_request( Method::Connect, vec![keys(3).public_key().to_hex(), "forbidden-secret".into()],