@@ -67,6 +67,21 @@ pub trait PublicSubkeyVerifier {
6767 ) -> Result < ( ) , SubkeyError > ;
6868}
6969
70+ /// Metadata bound to a subkey at derivation time.
71+ ///
72+ /// A subkey is derived from the master key using key tweaking (same math as BIP32 non-hardened,
73+ /// but using a BIP340-style tagged hash of this metadata as the tweak instead of a chaincode):
74+ ///
75+ /// ```text
76+ /// child_secret = master_secret + SHA256_tagged("Portal/Subkey", canonical_bytes(metadata)) (mod n)
77+ /// child_pubkey = master_pubkey + tweak·G
78+ /// ```
79+ ///
80+ /// This lets anyone with the master *public* key verify that a given subkey is legitimately
81+ /// derived — no private key is required on the server side.
82+ ///
83+ /// See [`SubkeyMetadata::canonical_bytes`] for the exact wire format and [`SubkeyMetadata::get_tweak`]
84+ /// for the tagged hash construction.
7085#[ derive( Debug , Clone , Serialize , Deserialize ) ]
7186#[ cfg_attr( feature = "bindings" , derive( uniffi:: Record ) ) ]
7287pub struct SubkeyMetadata {
@@ -79,19 +94,70 @@ pub struct SubkeyMetadata {
7994}
8095
8196impl SubkeyMetadata {
82- pub fn get_tweak ( & self ) -> Result < Scalar , SubkeyError > {
83- // Serialize metadata to bytes
84- let metadata_bytes = serde_json:: to_vec ( self ) ?;
97+ /// Produces a deterministic, cross-language binary encoding of the metadata.
98+ ///
99+ /// Layout (all integers little-endian):
100+ /// ```text
101+ /// version (1 byte, u8)
102+ /// name_len (2 bytes, u16 LE)
103+ /// name (name_len bytes, UTF-8)
104+ /// nonce (32 bytes)
105+ /// valid_from (8 bytes, u64 LE, Unix timestamp)
106+ /// expires_at (8 bytes, u64 LE, Unix timestamp)
107+ /// permissions (1 byte bitmask: Auth=0x01, Payment=0x02)
108+ /// ```
109+ ///
110+ /// This encoding is used as the message in [`get_tweak`] and must be reproduced
111+ /// identically by any verifier (mobile app, server) regardless of language or platform.
112+ pub fn canonical_bytes ( & self ) -> Vec < u8 > {
113+ let mut buf = Vec :: new ( ) ;
114+ buf. push ( self . version ) ;
115+ let name_bytes = self . name . as_bytes ( ) ;
116+ buf. extend_from_slice ( & ( name_bytes. len ( ) as u16 ) . to_le_bytes ( ) ) ;
117+ buf. extend_from_slice ( name_bytes) ;
118+ buf. extend_from_slice ( self . nonce . as_bytes ( ) ) ;
119+ buf. extend_from_slice ( & self . valid_from . as_u64 ( ) . to_le_bytes ( ) ) ;
120+ buf. extend_from_slice ( & self . expires_at . as_u64 ( ) . to_le_bytes ( ) ) ;
121+ let mut perms: u8 = 0 ;
122+ for p in & self . permissions {
123+ match p {
124+ SubkeyPermission :: Auth => perms |= 0x01 ,
125+ SubkeyPermission :: Payment => perms |= 0x02 ,
126+ }
127+ }
128+ buf. push ( perms) ;
129+ buf
130+ }
85131
86- // Compute the tweaking factor by hashing the metadata
132+ /// Computes the key tweak using BIP340-style tagged hashing (same construction as BIP341 Taproot).
133+ ///
134+ /// ```text
135+ /// tag = "Portal/Subkey"
136+ /// tag_hash = SHA256(tag)
137+ /// tweak = SHA256(tag_hash || tag_hash || canonical_bytes())
138+ /// ```
139+ ///
140+ /// The double-prefix `tag_hash || tag_hash` is the BIP340 tagged hash pattern — it provides
141+ /// domain separation so tweaks produced here cannot collide with those from other protocols.
142+ ///
143+ /// The resulting scalar is used to tweak both private and public keys:
144+ /// - Private: `child_secret = master_secret + tweak (mod n)`
145+ /// - Public: `child_pubkey = master_pubkey + tweak·G`
146+ ///
147+ /// This allows the server to verify a subkey using only the master *public* key —
148+ /// no private key is ever needed on the server side.
149+ pub fn get_tweak ( & self ) -> Result < Scalar , SubkeyError > {
150+ let tag = b"Portal/Subkey" ;
151+ let tag_hash: [ u8 ; 32 ] = Sha256 :: digest ( tag) . into ( ) ;
87152 let mut hasher = Sha256 :: new ( ) ;
88- hasher. update ( & metadata_bytes) ;
153+ hasher. update ( & tag_hash) ;
154+ hasher. update ( & tag_hash) ;
155+ hasher. update ( & self . canonical_bytes ( ) ) ;
89156 let hash: [ u8 ; 32 ] = hasher
90157 . finalize ( )
91158 . try_into ( )
92159 . map_err ( |_| SubkeyError :: InvalidMetadata ) ?;
93- let tweak = Scalar :: from_be_bytes ( hash) . map_err ( |_| SubkeyError :: InvalidMetadata ) ?;
94- Ok ( tweak)
160+ Scalar :: from_be_bytes ( hash) . map_err ( |_| SubkeyError :: InvalidMetadata )
95161 }
96162}
97163
@@ -108,9 +174,6 @@ pub enum SubkeyError {
108174 #[ error( "Invalid metadata" ) ]
109175 InvalidMetadata ,
110176
111- #[ error( "Serialization error: {0}" ) ]
112- Serialization ( #[ from] serde_json:: Error ) ,
113-
114177 #[ error( "Secp256k1 error: {0}" ) ]
115178 Secp256k1 ( #[ from] nostr:: secp256k1:: Error ) ,
116179
@@ -187,7 +250,7 @@ mod tests {
187250 valid_from : Timestamp :: new ( valid_from) ,
188251 expires_at : Timestamp :: new ( expires_at) ,
189252 permissions,
190- version : 1 ,
253+ version : 2 ,
191254 }
192255 }
193256
@@ -202,7 +265,7 @@ mod tests {
202265
203266 // Test that we can access both the metadata and the key methods
204267 assert_eq ! ( subkey. metadata( ) . name, "test_subkey" ) ;
205- assert_eq ! ( subkey. metadata( ) . version, 1 ) ;
268+ assert_eq ! ( subkey. metadata( ) . version, 2 ) ;
206269
207270 // Test that Deref works and we can use Keys methods directly
208271 let pubkey = subkey. public_key ( ) ;
0 commit comments