hns_chat_protocol/
owner.rs1use hns_service_authority::AuthorityRecord;
2use hns_transaction::Output;
3use k256::ecdsa::VerifyingKey;
4
5use crate::{ChatIdentityBindingV1, ChatProtocolError};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum ChatIdentityTrust {
9 ResourceAuthenticated,
10 CurrentOwnerVerified,
11 StaleOwner,
12 UnsupportedOwnerScript,
13}
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct VerifiedOwnerBindingV1 {
17 binding: ChatIdentityBindingV1,
18 original_compressed_public_key: [u8; 33],
19 trust: ChatIdentityTrust,
20}
21
22impl VerifiedOwnerBindingV1 {
23 pub const fn binding(&self) -> ChatIdentityBindingV1 {
24 self.binding
25 }
26
27 pub const fn original_compressed_public_key(&self) -> [u8; 33] {
28 self.original_compressed_public_key
29 }
30
31 pub const fn trust(&self) -> ChatIdentityTrust {
32 self.trust
33 }
34}
35
36pub fn xonly_from_compressed_public_key(key: &[u8; 33]) -> Result<[u8; 32], ChatProtocolError> {
37 if !matches!(key[0], 0x02 | 0x03) || VerifyingKey::from_sec1_bytes(key).is_err() {
38 return Err(ChatProtocolError::Invalid(
39 "invalid compressed secp256k1 public key",
40 ));
41 }
42 let mut xonly = [0_u8; 32];
43 xonly.copy_from_slice(&key[1..]);
44 Ok(xonly)
45}
46
47pub fn resolve_compressed_owner_key(
48 owner_output: &Output,
49 xonly_public_key: &[u8; 32],
50) -> Result<[u8; 33], ChatProtocolError> {
51 if owner_output.address.version != 0 || owner_output.address.hash.len() != 20 {
52 return Err(ChatProtocolError::UnsupportedOwnerScript);
53 }
54
55 let mut even_candidate = [0_u8; 33];
56 even_candidate[0] = 0x02;
57 even_candidate[1..].copy_from_slice(xonly_public_key);
58 VerifyingKey::from_sec1_bytes(&even_candidate)
59 .map_err(|_| ChatProtocolError::Invalid("invalid secp256k1 x-only public key"))?;
60
61 let mut matched = None;
62 for parity in [0x02, 0x03] {
63 let mut candidate = [0_u8; 33];
64 candidate[0] = parity;
65 candidate[1..].copy_from_slice(xonly_public_key);
66 if VerifyingKey::from_sec1_bytes(&candidate).is_err() {
67 continue;
68 }
69 let candidate_address = hns_transaction::Address::from_compressed_public_key(&candidate)
70 .map_err(|_| ChatProtocolError::Invalid("owner public key cannot form an address"))?;
71 if candidate_address.version == owner_output.address.version
72 && candidate_address.hash == owner_output.address.hash
73 && matched.replace(candidate).is_some()
74 {
75 return Err(ChatProtocolError::AmbiguousOwnerKey);
76 }
77 }
78 matched.ok_or(ChatProtocolError::StaleOwner)
79}
80
81pub fn verify_current_owner_binding(
82 binding: &ChatIdentityBindingV1,
83 owner_output: &Output,
84) -> Result<VerifiedOwnerBindingV1, ChatProtocolError> {
85 binding.validate()?;
86 let original_compressed_public_key =
87 resolve_compressed_owner_key(owner_output, &binding.xonly_public_key)?;
88 Ok(VerifiedOwnerBindingV1 {
89 binding: *binding,
90 original_compressed_public_key,
91 trust: ChatIdentityTrust::CurrentOwnerVerified,
92 })
93}
94
95pub fn owner_authority_record(
96 verified: &VerifiedOwnerBindingV1,
97) -> Result<AuthorityRecord, ChatProtocolError> {
98 if verified.trust != ChatIdentityTrust::CurrentOwnerVerified
99 || verified.binding.generation == 0
100 || xonly_from_compressed_public_key(&verified.original_compressed_public_key)?
101 != verified.binding.xonly_public_key
102 {
103 return Err(ChatProtocolError::Invalid(
104 "owner authority requires a current verified binding",
105 ));
106 }
107 Ok(AuthorityRecord {
108 root_key: verified.original_compressed_public_key,
109 epoch: verified.binding.generation,
110 })
111}
112
113#[cfg(test)]
114mod tests {
115 use hns_covenants::{Covenant, CovenantKind};
116 use hns_primitives::Dollarydoos;
117 use k256::ecdsa::SigningKey;
118
119 use super::*;
120
121 fn owner_output(public_key: &[u8; 33]) -> Output {
122 Output {
123 value: Dollarydoos::new(1),
124 address: hns_transaction::Address::from_compressed_public_key(public_key)
125 .expect("address"),
126 covenant: Covenant {
127 kind: CovenantKind::Update,
128 items: Vec::new(),
129 },
130 }
131 }
132
133 fn public_key(private_key: [u8; 32]) -> [u8; 33] {
134 SigningKey::from_bytes((&private_key).into())
135 .expect("private key")
136 .verifying_key()
137 .to_encoded_point(true)
138 .as_bytes()
139 .try_into()
140 .expect("compressed key")
141 }
142
143 #[test]
144 fn both_original_owner_parities_are_recovered_without_normalization() {
145 let mut seen_even = false;
146 let mut seen_odd = false;
147 for scalar in 1_u8..=32 {
148 let mut private_key = [0_u8; 32];
149 private_key[31] = scalar;
150 let compressed = public_key(private_key);
151 seen_even |= compressed[0] == 0x02;
152 seen_odd |= compressed[0] == 0x03;
153 let binding = ChatIdentityBindingV1 {
154 key_mode: crate::ChatKeyMode::Owner,
155 xonly_public_key: xonly_from_compressed_public_key(&compressed).expect("x-only"),
156 generation: u32::from(scalar),
157 };
158 let verified = verify_current_owner_binding(&binding, &owner_output(&compressed))
159 .expect("current owner");
160 assert_eq!(verified.original_compressed_public_key(), compressed);
161 assert_eq!(
162 owner_authority_record(&verified).expect("authority"),
163 AuthorityRecord {
164 root_key: compressed,
165 epoch: u32::from(scalar),
166 }
167 );
168 if seen_even && seen_odd {
169 break;
170 }
171 }
172 assert!(seen_even && seen_odd, "test keys must cover both parities");
173 }
174
175 #[test]
176 fn stale_and_script_controlled_owner_outputs_are_rejected() {
177 let key = public_key([1; 32]);
178 let other_key = public_key([2; 32]);
179 assert_eq!(
180 resolve_compressed_owner_key(
181 &owner_output(&other_key),
182 &xonly_from_compressed_public_key(&key).expect("x-only")
183 ),
184 Err(ChatProtocolError::StaleOwner)
185 );
186 let mut script_owner = owner_output(&key);
187 script_owner.address = hns_transaction::Address::new(0, vec![1; 32]).expect("P2WSH");
188 assert_eq!(
189 resolve_compressed_owner_key(
190 &script_owner,
191 &xonly_from_compressed_public_key(&key).expect("x-only")
192 ),
193 Err(ChatProtocolError::UnsupportedOwnerScript)
194 );
195 }
196
197 #[test]
198 fn invalid_x_coordinate_is_rejected() {
199 let key = public_key([3; 32]);
200 assert!(resolve_compressed_owner_key(&owner_output(&key), &[0xff; 32]).is_err());
201 }
202}