Skip to main content

hns_chat_protocol/
owner.rs

1use 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}