myc

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

commit 60bffa22c8d435cb1426a8460dc7cfddbf6eea2f
parent 8c75251a3ebbbc07f99504b09583892b36906e1c
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 22:11:40 +0000

refactor: adopt context-bound envelopes

Diffstat:
MCargo.lock | 27+++++++++++++++++----------
MCargo.toml | 12++++++------
Msrc/identity_files.rs | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/signing_adapter.rs | 32++++++++++++++++++--------------
4 files changed, 283 insertions(+), 50 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1719,7 +1719,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_blossom" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "mediatype", "serde", @@ -1731,7 +1731,7 @@ dependencies = [ [[package]] name = "radroots_core" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "rust_decimal", "serde", @@ -1740,7 +1740,7 @@ dependencies = [ [[package]] name = "radroots_event" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "hex", "jiff-tzdb", @@ -1758,8 +1758,9 @@ dependencies = [ [[package]] name = "radroots_event_codec" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ + "hex", "radroots_blossom", "radroots_core", "radroots_event", @@ -1768,12 +1769,13 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "sha2", ] [[package]] name = "radroots_identity" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "k256", "serde", @@ -1783,7 +1785,7 @@ dependencies = [ [[package]] name = "radroots_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "nostr", "radroots_event", @@ -1797,7 +1799,7 @@ dependencies = [ [[package]] name = "radroots_nostr_connect" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "nostr", "radroots_event", @@ -1813,7 +1815,7 @@ dependencies = [ [[package]] name = "radroots_protocol" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "serde", ] @@ -1821,23 +1823,28 @@ dependencies = [ [[package]] name = "radroots_secrets" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "chacha20poly1305", "keyring", "serde", + "sha2", + "subtle", "zeroize", ] [[package]] name = "radroots_signing" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=74f5c91cbc80248df1f51f644004daaf8ae8e9be#74f5c91cbc80248df1f51f644004daaf8ae8e9be" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ + "hex", "radroots_event", + "radroots_event_codec", "radroots_identity", "radroots_protocol", "serde", + "sha2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -23,12 +23,12 @@ hex = "0.4" keyring = { version = "3.6", features = ["apple-native", "windows-native", "sync-secret-service"] } nostr = { version = "0.44.2", features = ["nip04", "nip44", "nip46", "nip49"] } nostr-sdk = { version = "0.44.1" } -radroots_identity = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha" } -radroots_event = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha", features = ["serde"] } -radroots_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha", features = ["events"] } -radroots_nostr_connect = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha" } -radroots_secrets = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha", features = ["std", "keyring"] } -radroots_signing = { git = "https://github.com/radrootslabs/lib.git", rev = "74f5c91cbc80248df1f51f644004daaf8ae8e9be", version = "=0.1.0-alpha", features = ["std"] } +radroots_identity = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_event = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", features = ["serde"] } +radroots_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", features = ["events"] } +radroots_nostr_connect = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_secrets = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", features = ["std", "keyring"] } +radroots_signing = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", features = ["std"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10" diff --git a/src/identity_files.rs b/src/identity_files.rs @@ -5,11 +5,17 @@ use std::path::{Path, PathBuf}; use chacha20poly1305::aead::{Aead, KeyInit, Payload}; use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; -use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; +use radroots_secrets::envelope::{ + ENVELOPE_VERSION, LEGACY_ENVELOPE_VERSION, LegacyV1ResealAuthority, Nonce, SealMaterial, + SealRequest, +}; use radroots_secrets::error::Operation; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::wrapping::{ - BoxFuture, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, + BoxFuture, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, }; use radroots_secrets::{EncryptedEnvelope, KeyWrapping, SecretId, SecretRef}; use zeroize::Zeroize; @@ -21,7 +27,9 @@ use crate::host_identity::{ const MYC_IDENTITY_KEY_SLOT: &str = "myc_identity"; const WRAPPING_KEY_BYTES: usize = 32; const WRAPPING_NONCE_BYTES: usize = 24; -const WRAPPED_KEY_VERSION: u8 = 1; +const LEGACY_WRAPPED_KEY_VERSION: u8 = 1; +const WRAPPED_KEY_VERSION: u8 = 2; +const WRAPPING_AAD_DOMAIN: &[u8] = b"myc.wrapped_data_key.v2"; struct MycFileKeyWrapping { key_path: PathBuf, @@ -81,14 +89,16 @@ impl KeyWrapping for MycFileKeyWrapping { request: WrapRequest<'a>, ) -> BoxFuture<'a, Result<WrappedSecret, radroots_secrets::Error>> { Box::pin(async move { + validate_identity_reference(request.reference(), Operation::Wrap)?; let mut key = self.load_or_create_key()?; let nonce: [u8; WRAPPING_NONCE_BYTES] = rand::random(); + let aad = wrapping_aad(request.reference(), request.context()); let ciphertext = request.plaintext().expose_secret(|plaintext| { XChaCha20Poly1305::new(Key::from_slice(&key)).encrypt( XNonce::from_slice(&nonce), Payload { msg: plaintext, - aad: request.reference().id().as_str().as_bytes(), + aad: aad.as_slice(), }, ) }); @@ -107,26 +117,81 @@ impl KeyWrapping for MycFileKeyWrapping { request: UnwrapRequest<'a>, ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> { Box::pin(async move { - let wrapped = request.wrapped().as_bytes(); - if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != WRAPPED_KEY_VERSION { - return Err(secret_backend_failure(Operation::Unwrap)); - } - let mut key = self.load_key()?; - let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt( - XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]), - Payload { - msg: &wrapped[1 + WRAPPING_NONCE_BYTES..], - aad: request.reference().id().as_str().as_bytes(), - }, - ); - key.zeroize(); - SecretMaterial::from_slice( - &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?, + validate_identity_reference(request.reference(), Operation::Unwrap)?; + let aad = wrapping_aad(request.reference(), request.context()); + self.unwrap_with_aad(request.wrapped(), WRAPPED_KEY_VERSION, aad.as_slice()) + }) + } + + fn unwrap_legacy_v1<'a>( + &'a self, + request: LegacyV1UnwrapRequest<'a>, + ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> { + Box::pin(async move { + validate_identity_reference(request.reference(), Operation::Unwrap)?; + self.unwrap_with_aad( + request.wrapped(), + LEGACY_WRAPPED_KEY_VERSION, + request.reference().id().as_str().as_bytes(), ) }) } } +impl MycFileKeyWrapping { + fn unwrap_with_aad( + &self, + wrapped: &WrappedSecret, + expected_version: u8, + aad: &[u8], + ) -> Result<SecretMaterial, radroots_secrets::Error> { + let wrapped = wrapped.as_bytes(); + if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != expected_version { + return Err(secret_backend_failure(Operation::Unwrap)); + } + let mut key = self.load_key()?; + let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt( + XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]), + Payload { + msg: &wrapped[1 + WRAPPING_NONCE_BYTES..], + aad, + }, + ); + key.zeroize(); + SecretMaterial::from_slice( + &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?, + ) + } +} + +fn wrapping_aad(reference: &SecretRef, context: &EnvelopeContext) -> Vec<u8> { + let id = reference.id().as_str().as_bytes(); + let mut aad = Vec::with_capacity(WRAPPING_AAD_DOMAIN.len() + 2 + id.len() + 4 + 32); + aad.extend_from_slice(WRAPPING_AAD_DOMAIN); + aad.extend_from_slice( + &u16::try_from(id.len()) + .expect("validated secret identifier length fits u16") + .to_be_bytes(), + ); + aad.extend_from_slice(id); + aad.extend_from_slice(&reference.key_version().get().to_be_bytes()); + aad.extend_from_slice(&context.authentication_digest()); + aad +} + +fn validate_identity_reference( + reference: &SecretRef, + operation: Operation, +) -> Result<(), radroots_secrets::Error> { + if reference.backend() != BackendKind::External + || reference.key_version().get() != 1 + || reference.id().as_str() != MYC_IDENTITY_KEY_SLOT + { + return Err(secret_backend_failure(operation)); + } + Ok(()) +} + fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> { Ok(SecretRef::new( SecretId::parse(MYC_IDENTITY_KEY_SLOT)?, @@ -135,6 +200,14 @@ fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> { )) } +fn identity_envelope_context() -> Result<EnvelopeContext, radroots_secrets::Error> { + Ok(EnvelopeContext::new( + EnvelopePurpose::parse("radroots.service_identity")?, + EnvelopeSubject::parse("service", "myc")?, + PayloadSchemaId::parse("radroots.myc_identity.v1")?, + )) +} + fn secret_backend_failure(operation: Operation) -> radroots_secrets::Error { radroots_secrets::Error::BackendFailure { backend: BackendKind::External, @@ -175,10 +248,13 @@ pub fn store_encrypted_identity( let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>()) .map_err(|_| storage_error(path, "validate identity data key"))?; let wrapping = MycFileKeyWrapping::new(path); + let context = + identity_envelope_context().map_err(|_| storage_error(path, "build identity context"))?; let envelope = futures_executor::block_on(EncryptedEnvelope::seal( &wrapping, SealRequest::new( identity_secret_ref().map_err(|_| storage_error(path, "build identity reference"))?, + context, &plaintext, SealMaterial::new(data_key, Nonce::new(rand::random())), ), @@ -202,7 +278,27 @@ pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentit let envelope = EncryptedEnvelope::decode(encoded.as_slice()) .map_err(|_| storage_error(path, "decode encrypted identity"))?; let wrapping = MycFileKeyWrapping::new(path); - let payload = futures_executor::block_on(envelope.open(&wrapping)) + let context = + identity_envelope_context().map_err(|_| storage_error(path, "build identity context"))?; + if envelope.version() == LEGACY_ENVELOPE_VERSION { + return migrate_legacy_identity(path, envelope, &wrapping, context); + } + if envelope.version() != ENVELOPE_VERSION { + return Err(storage_error( + path, + "unsupported encrypted identity version", + )); + } + open_identity(path, &envelope, &wrapping, &context) +} + +fn open_identity( + path: &Path, + envelope: &EncryptedEnvelope, + wrapping: &MycFileKeyWrapping, + context: &EnvelopeContext, +) -> Result<RadrootsIdentity, IdentityError> { + let payload = futures_executor::block_on(envelope.open(wrapping, context)) .map_err(|_| storage_error(path, "open encrypted identity"))?; let file: RadrootsIdentityFile = payload .expose_secret(|bytes| serde_json::from_slice(bytes)) @@ -210,6 +306,42 @@ pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentit RadrootsIdentity::try_from(file) } +fn migrate_legacy_identity( + path: &Path, + envelope: EncryptedEnvelope, + wrapping: &MycFileKeyWrapping, + context: EnvelopeContext, +) -> Result<RadrootsIdentity, IdentityError> { + let expected_reference = + identity_secret_ref().map_err(|_| storage_error(path, "build identity reference"))?; + let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>()) + .map_err(|_| storage_error(path, "validate identity data key"))?; + let resealed = futures_executor::block_on(envelope.reseal_legacy_v1( + wrapping, + &LegacyV1ResealAuthority::new(), + &expected_reference, + identity_secret_ref().map_err(|_| storage_error(path, "build identity reference"))?, + context.clone(), + &valid_identity_payload, + SealMaterial::new(data_key, Nonce::new(rand::random())), + )) + .map_err(|_| storage_error(path, "migrate legacy encrypted identity"))?; + let envelope = resealed.into_envelope(); + let identity = open_identity(path, &envelope, wrapping, &context)?; + let encoded = envelope + .encode() + .map_err(|_| storage_error(path, "encode migrated identity"))?; + atomic_write(path, encoded.as_slice())?; + Ok(identity) +} + +fn valid_identity_payload(bytes: &[u8]) -> bool { + serde_json::from_slice::<RadrootsIdentityFile>(bytes) + .ok() + .and_then(|file| RadrootsIdentity::try_from(file).ok()) + .is_some() +} + pub fn rotate_encrypted_identity(path: impl AsRef<Path>) -> Result<(), IdentityError> { let path = path.as_ref(); let identity = load_encrypted_identity(path)?; @@ -325,6 +457,36 @@ mod tests { load_encrypted_identity(&path).expect("load").id(), identity.id() ); + let envelope = + EncryptedEnvelope::decode(&fs::read(&path).expect("read encrypted identity")) + .expect("decode encrypted identity"); + assert_eq!(envelope.version(), ENVELOPE_VERSION); + assert_eq!( + envelope.context(), + Some(&identity_envelope_context().expect("identity context")) + ); + } + + #[test] + fn encrypted_identity_migrates_legacy_v1() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.enc"); + let identity = identity(); + store_legacy_identity(&path, &identity); + + assert_eq!( + load_encrypted_identity(&path) + .expect("migrate legacy identity") + .id(), + identity.id() + ); + let envelope = EncryptedEnvelope::decode(&fs::read(&path).expect("read migrated identity")) + .expect("decode migrated identity"); + assert_eq!(envelope.version(), ENVELOPE_VERSION); + assert_eq!( + envelope.context(), + Some(&identity_envelope_context().expect("identity context")) + ); } #[test] @@ -338,4 +500,64 @@ mod tests { identity.id() ); } + + fn store_legacy_identity(path: &Path, identity: &RadrootsIdentity) { + const NONCE_BYTES: usize = 24; + const TAG_BYTES: usize = 16; + + let wrapping_key = [0x11; 32]; + let data_key = [0x22; 32]; + let wrapping_nonce = [0x33; NONCE_BYTES]; + let envelope_nonce = [0x44; NONCE_BYTES]; + let payload = serde_json::to_vec(&identity.to_file()).expect("encode identity payload"); + let wrapped_ciphertext = XChaCha20Poly1305::new(Key::from_slice(&wrapping_key)) + .encrypt( + XNonce::from_slice(&wrapping_nonce), + Payload { + msg: &data_key, + aad: MYC_IDENTITY_KEY_SLOT.as_bytes(), + }, + ) + .expect("wrap legacy data key"); + let mut wrapped = Vec::with_capacity(1 + NONCE_BYTES + wrapped_ciphertext.len()); + wrapped.push(LEGACY_WRAPPED_KEY_VERSION); + wrapped.extend_from_slice(&wrapping_nonce); + wrapped.extend_from_slice(&wrapped_ciphertext); + + let id = MYC_IDENTITY_KEY_SLOT.as_bytes(); + let mut encoded = Vec::new(); + encoded.extend_from_slice(b"RRS1"); + encoded.extend_from_slice(&LEGACY_ENVELOPE_VERSION.to_be_bytes()); + encoded.extend_from_slice(&[1, 1, 4]); + encoded.extend_from_slice(&1_u32.to_be_bytes()); + encoded.extend_from_slice(&u16::try_from(id.len()).expect("id length").to_be_bytes()); + encoded.extend_from_slice(id); + encoded.extend_from_slice(&envelope_nonce); + encoded.extend_from_slice( + &u32::try_from(wrapped.len()) + .expect("wrapped length") + .to_be_bytes(), + ); + encoded.extend_from_slice(&wrapped); + encoded.extend_from_slice( + &u32::try_from(payload.len() + TAG_BYTES) + .expect("ciphertext length") + .to_be_bytes(), + ); + let ciphertext = XChaCha20Poly1305::new(Key::from_slice(&data_key)) + .encrypt( + XNonce::from_slice(&envelope_nonce), + Payload { + msg: &payload, + aad: &encoded, + }, + ) + .expect("encrypt legacy payload"); + encoded.extend_from_slice(&ciphertext); + + fs::write(path, encoded).expect("write legacy envelope"); + let key_path = encrypted_identity_wrapping_key_path(path); + fs::write(&key_path, wrapping_key).expect("write wrapping key"); + set_secret_permissions(&key_path).expect("secure wrapping key"); + } } diff --git a/src/signing_adapter.rs b/src/signing_adapter.rs @@ -6,6 +6,7 @@ use nostr::{EventBuilder, JsonUtil, Kind, Tag, Timestamp}; use radroots_event::{SignedEvent, wire::v1::Nip01EventWire}; use radroots_signing::capability::{CancellationSupport, SignerCapability, SignerKind}; use radroots_signing::error::Kind as SigningErrorKind; +use radroots_signing::recovery::ReplayCapability; use radroots_signing::status::{SignProgress, SignProgressStage, SignerAvailability}; use radroots_signing::{Error, SignReceipt, SignRequest, Signer, SignerStatus}; @@ -18,6 +19,7 @@ impl Signer for MycActiveIdentity { SignerAvailability::Ready, vec![SignerCapability::new( SignerKind::HostMediated, + ReplayCapability::LocalReplaySafe, CancellationSupport::BeforePublication, true, true, @@ -32,27 +34,27 @@ impl Signer for MycActiveIdentity { request: SignRequest, ) -> radroots_signing::signer::BoxFuture<'_, Result<SignReceipt, Error>> { Box::pin(async move { - let now = now_unix_secs(); - if now > request.policy().deadline_unix() { - return Err(Error::new(SigningErrorKind::DeadlineExceeded)); - } - if request.draft().expected_pubkey() != &self.public_identity().public_key() { + let now = now_unix_ms(); + request.ensure_active(now)?; + let plan = request.plan(); + if plan.author() != &self.public_identity().public_key() { return Err(Error::new(SigningErrorKind::AuthorizationDenied)); } report_progress(&request, SignProgressStage::Validating)?; - let kind = u16::try_from(request.draft().kind_u32()) + let kind = u16::try_from(plan.body().kind()) .map_err(|_| Error::new(SigningErrorKind::InvalidArgument))?; - let tags = request - .draft() - .tags_as_vec() - .into_iter() + let tags = plan + .body() + .tags() + .iter() + .cloned() .map(Tag::parse) .collect::<Result<Vec<_>, _>>() .map_err(|source| Error::with_source(SigningErrorKind::InvalidArgument, source))?; - let unsigned = EventBuilder::new(Kind::Custom(kind), request.draft().content()) + let unsigned = EventBuilder::new(Kind::Custom(kind), plan.body().content()) .tags(tags) - .custom_created_at(Timestamp::from(request.draft().created_at_u64())) + .custom_created_at(Timestamp::from(plan.created_at())) .build(self.public_key()); let event = self .sign_unsigned_event(unsigned, "final signing request") @@ -79,8 +81,10 @@ fn report_progress(request: &SignRequest, stage: SignProgressStage) -> Result<() Ok(()) } -fn now_unix_secs() -> u64 { +fn now_unix_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) - .map_or(0, |duration| duration.as_secs()) + .map_or(0, |duration| { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) + }) }