Skip to main content

chains_sdk/neo/
mod.rs

1//! NEO ECDSA signer using NIST P-256 (secp256r1) + SHA-256.
2
3pub mod transaction;
4pub mod witness;
5
6use crate::crypto;
7use crate::error::SignerError;
8use crate::traits;
9use p256::ecdsa::signature::hazmat::PrehashSigner;
10use p256::ecdsa::signature::hazmat::PrehashVerifier;
11use p256::ecdsa::{Signature as P256Signature, SigningKey, VerifyingKey};
12use zeroize::Zeroizing;
13
14/// A NEO ECDSA signature (64 bytes, r || s).
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[must_use]
18pub struct NeoSignature {
19    /// 64 bytes: r (32) || s (32).
20    #[cfg_attr(feature = "serde", serde(with = "crate::hex_bytes"))]
21    pub bytes: [u8; 64],
22}
23
24impl core::fmt::Display for NeoSignature {
25    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26        write!(f, "0x")?;
27        for byte in &self.bytes {
28            write!(f, "{byte:02x}")?;
29        }
30        Ok(())
31    }
32}
33
34impl NeoSignature {
35    /// Export the 64-byte r||s signature.
36    pub fn to_bytes(&self) -> [u8; 64] {
37        self.bytes
38    }
39
40    /// Import from 64 bytes.
41    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
42        if bytes.len() != 64 {
43            return Err(SignerError::InvalidSignature(format!(
44                "expected 64 bytes, got {}",
45                bytes.len()
46            )));
47        }
48        let mut out = [0u8; 64];
49        out.copy_from_slice(bytes);
50        Ok(Self { bytes: out })
51    }
52}
53
54/// NEO ECDSA signer using NIST P-256 (secp256r1).
55pub struct NeoSigner {
56    signing_key: SigningKey,
57}
58
59impl NeoSigner {
60    /// Compute the NEO script hash from the compressed public key.
61    ///
62    /// NEO N3 standard: `HASH160(0x21 || compressed_pubkey || 0xAC)`
63    /// where `0x21` = PUSH33 and `0xAC` = CHECKSIG.
64    pub fn script_hash(&self) -> [u8; 20] {
65        let pubkey = self.signing_key.verifying_key().to_encoded_point(true);
66        let mut script = Vec::with_capacity(35);
67        script.push(0x21); // PUSH33 opcode
68        script.extend_from_slice(pubkey.as_bytes());
69        script.push(0xAC); // CHECKSIG opcode
70
71        crypto::hash160(&script)
72    }
73
74    /// Return the NEO `A...` address string.
75    ///
76    /// Formula: Base58Check(0x17 || script_hash)
77    pub fn address(&self) -> String {
78        let hash = self.script_hash();
79        let mut payload = vec![0x17u8]; // NEO version byte
80        payload.extend_from_slice(&hash);
81        let checksum = crypto::double_sha256(&payload);
82        payload.extend_from_slice(&checksum[..4]);
83        bs58::encode(payload).into_string()
84    }
85}
86
87/// Validate a NEO `A...` address string.
88///
89/// Checks: starts with 'A', 25-byte Base58Check decode, version 0x17, valid checksum.
90pub fn validate_address(address: &str) -> bool {
91    if !address.starts_with('A') {
92        return false;
93    }
94    let decoded = match bs58::decode(address).into_vec() {
95        Ok(d) => d,
96        Err(_) => return false,
97    };
98    if decoded.len() != 25 || decoded[0] != 0x17 {
99        return false;
100    }
101    use subtle::ConstantTimeEq;
102    let checksum = crypto::double_sha256(&decoded[..21]);
103    checksum[..4].ct_eq(&decoded[21..25]).unwrap_u8() == 1
104}
105
106impl NeoSigner {
107    /// Export the private key in **WIF** (Wallet Import Format).
108    ///
109    /// Uses version byte 0x80 with compression flag 0x01.
110    /// Result is a Base58Check-encoded string.
111    ///
112    /// # Security
113    /// The returned String contains the private key — handle with care.
114    pub fn to_wif(&self) -> Zeroizing<String> {
115        let pk_bytes = self.signing_key.to_bytes();
116        let mut payload = Vec::with_capacity(38);
117        payload.push(0x80); // version
118        payload.extend_from_slice(&pk_bytes);
119        payload.push(0x01); // compression flag
120
121        let checksum = crypto::double_sha256(&payload);
122        payload.extend_from_slice(&checksum[..4]);
123        Zeroizing::new(bs58::encode(payload).into_string())
124    }
125
126    /// Import a private key from **WIF** (Wallet Import Format).
127    ///
128    /// Accepts NEO WIF strings (version 0x80 with compression flag).
129    pub fn from_wif(wif: &str) -> Result<Self, SignerError> {
130        let decoded = bs58::decode(wif)
131            .into_vec()
132            .map_err(|_| SignerError::InvalidPrivateKey("invalid Base58".into()))?;
133
134        if decoded.len() != 38 {
135            return Err(SignerError::InvalidPrivateKey(format!(
136                "WIF: expected 38 bytes, got {}",
137                decoded.len()
138            )));
139        }
140
141        if decoded[0] != 0x80 {
142            return Err(SignerError::InvalidPrivateKey(format!(
143                "WIF: version 0x{:02x} != 0x80",
144                decoded[0]
145            )));
146        }
147
148        if decoded[33] != 0x01 {
149            return Err(SignerError::InvalidPrivateKey(
150                "WIF: missing compression flag".into(),
151            ));
152        }
153
154        // Verify checksum (constant-time comparison)
155        let checksum = crypto::double_sha256(&decoded[..34]);
156        use subtle::ConstantTimeEq;
157        if decoded[34..38].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
158            return Err(SignerError::InvalidPrivateKey("WIF: bad checksum".into()));
159        }
160
161        use crate::traits::KeyPair;
162        Self::from_bytes(&decoded[1..33])
163    }
164}
165
166impl Drop for NeoSigner {
167    fn drop(&mut self) {
168        // p256::SigningKey implements ZeroizeOnDrop internally
169    }
170}
171
172impl NeoSigner {
173    pub(crate) fn sign_digest(&self, digest: &[u8; 32]) -> Result<NeoSignature, SignerError> {
174        let sig: P256Signature = self
175            .signing_key
176            .sign_prehash(digest)
177            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
178        let mut bytes = [0u8; 64];
179        bytes.copy_from_slice(&sig.to_bytes());
180        Ok(NeoSignature { bytes })
181    }
182}
183
184impl traits::Signer for NeoSigner {
185    type Signature = NeoSignature;
186    type Error = SignerError;
187
188    fn sign(&self, message: &[u8]) -> Result<NeoSignature, SignerError> {
189        let hash = crypto::sha256(message);
190        self.sign_digest(&hash)
191    }
192
193    fn sign_prehashed(&self, digest: &[u8]) -> Result<NeoSignature, SignerError> {
194        if digest.len() != 32 {
195            return Err(SignerError::InvalidHashLength {
196                expected: 32,
197                got: digest.len(),
198            });
199        }
200        let mut hash = [0u8; 32];
201        hash.copy_from_slice(digest);
202        self.sign_digest(&hash)
203    }
204
205    fn public_key_bytes(&self) -> Vec<u8> {
206        self.signing_key
207            .verifying_key()
208            .to_encoded_point(true)
209            .as_bytes()
210            .to_vec()
211    }
212
213    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
214        self.signing_key
215            .verifying_key()
216            .to_encoded_point(false)
217            .as_bytes()
218            .to_vec()
219    }
220}
221
222impl traits::KeyPair for NeoSigner {
223    fn generate() -> Result<Self, SignerError> {
224        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
225        crate::security::secure_random(&mut *key_bytes)?;
226        let signing_key = SigningKey::from_bytes((&*key_bytes).into())
227            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
228        Ok(Self { signing_key })
229    }
230
231    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
232        if private_key.len() != 32 {
233            return Err(SignerError::InvalidPrivateKey(format!(
234                "expected 32 bytes, got {}",
235                private_key.len()
236            )));
237        }
238        let signing_key = SigningKey::from_bytes(private_key.into())
239            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
240        Ok(Self { signing_key })
241    }
242
243    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
244        Zeroizing::new(self.signing_key.to_bytes().to_vec())
245    }
246}
247
248/// NEO ECDSA verifier (P-256).
249pub struct NeoVerifier {
250    verifying_key: VerifyingKey,
251}
252
253impl NeoVerifier {
254    /// Create from SEC1 public key bytes.
255    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
256        let verifying_key = VerifyingKey::from_sec1_bytes(bytes)
257            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
258        Ok(Self { verifying_key })
259    }
260
261    fn verify_digest(
262        &self,
263        digest: &[u8; 32],
264        signature: &NeoSignature,
265    ) -> Result<bool, SignerError> {
266        let sig = P256Signature::from_bytes((&signature.bytes).into())
267            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
268        match self.verifying_key.verify_prehash(digest, &sig) {
269            Ok(()) => Ok(true),
270            Err(_) => Ok(false),
271        }
272    }
273}
274
275impl traits::Verifier for NeoVerifier {
276    type Signature = NeoSignature;
277    type Error = SignerError;
278
279    fn verify(&self, message: &[u8], signature: &NeoSignature) -> Result<bool, SignerError> {
280        let hash = crypto::sha256(message);
281        self.verify_digest(&hash, signature)
282    }
283
284    fn verify_prehashed(
285        &self,
286        digest: &[u8],
287        signature: &NeoSignature,
288    ) -> Result<bool, SignerError> {
289        if digest.len() != 32 {
290            return Err(SignerError::InvalidHashLength {
291                expected: 32,
292                got: digest.len(),
293            });
294        }
295        let mut hash = [0u8; 32];
296        hash.copy_from_slice(digest);
297        self.verify_digest(&hash, signature)
298    }
299}
300
301#[cfg(test)]
302#[allow(clippy::unwrap_used, clippy::expect_used)]
303mod tests {
304    use super::*;
305    use crate::traits::{KeyPair, Signer, Verifier};
306
307    #[test]
308    fn test_generate_keypair() {
309        let signer = NeoSigner::generate().unwrap();
310        let pubkey = signer.public_key_bytes();
311        assert_eq!(pubkey.len(), 33); // compressed P-256
312    }
313
314    #[test]
315    fn test_from_bytes_roundtrip() {
316        let signer = NeoSigner::generate().unwrap();
317        let restored = NeoSigner::from_bytes(&signer.private_key_bytes()).unwrap();
318        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
319    }
320
321    #[test]
322    fn test_sign_verify_roundtrip() {
323        let signer = NeoSigner::generate().unwrap();
324        let sig = signer.sign(b"hello neo").unwrap();
325        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
326        assert!(verifier.verify(b"hello neo", &sig).unwrap());
327    }
328
329    #[test]
330    fn test_p256_not_k256() {
331        // P-256 pubkeys are different from secp256k1 for the same private key bytes
332        let privkey =
333            hex::decode("708309a7449e156b0db70e5b52e606c7e094ed676ce8953bf6c14757c826f590")
334                .unwrap();
335        let neo_signer = NeoSigner::from_bytes(&privkey).unwrap();
336        let neo_pubkey = neo_signer.public_key_bytes();
337        // This is a P-256 pubkey, not secp256k1
338        assert_eq!(neo_pubkey.len(), 33);
339    }
340
341    #[test]
342    fn test_neo_serialization() {
343        let signer = NeoSigner::generate().unwrap();
344        let sig = signer.sign(b"serialization test").unwrap();
345        assert_eq!(sig.bytes.len(), 64); // r || s
346    }
347
348    // FIPS 186-4 / NIST CAVP P-256 Test Vector
349    #[test]
350    fn test_known_vector_p256_fips() {
351        let privkey =
352            hex::decode("708309a7449e156b0db70e5b52e606c7e094ed676ce8953bf6c14757c826f590")
353                .unwrap();
354        let signer = NeoSigner::from_bytes(&privkey).unwrap();
355        // Sign a test message and verify round-trip
356        let sig = signer.sign(b"NIST P-256 test").unwrap();
357        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
358        assert!(verifier.verify(b"NIST P-256 test", &sig).unwrap());
359    }
360
361    #[test]
362    fn test_invalid_privkey_rejected() {
363        assert!(NeoSigner::from_bytes(&[0u8; 32]).is_err());
364        assert!(NeoSigner::from_bytes(&[1u8; 31]).is_err());
365    }
366
367    #[test]
368    fn test_tampered_sig_fails() {
369        let signer = NeoSigner::generate().unwrap();
370        let sig = signer.sign(b"tamper").unwrap();
371        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
372        let mut tampered = sig.clone();
373        tampered.bytes[0] ^= 0xff;
374        let result = verifier.verify(b"tamper", &tampered);
375        assert!(result.is_err() || !result.unwrap());
376    }
377
378    #[test]
379    fn test_sign_prehashed_roundtrip() {
380        let signer = NeoSigner::generate().unwrap();
381        let msg = b"prehash neo";
382        let digest = crate::crypto::sha256(msg);
383        let sig = signer.sign_prehashed(&digest).unwrap();
384        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
385        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
386    }
387
388    #[test]
389    fn test_zeroize_on_drop() {
390        let signer = NeoSigner::generate().unwrap();
391        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
392        drop(signer);
393    }
394
395    #[test]
396    fn test_empty_message() {
397        let signer = NeoSigner::generate().unwrap();
398        let sig = signer.sign(b"").unwrap();
399        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
400        assert!(verifier.verify(b"", &sig).unwrap());
401    }
402
403    // ─── NEO Known Address Vectors ──────────────────────────────
404
405    #[test]
406    fn test_neo_address_format() {
407        let signer = NeoSigner::generate().unwrap();
408        let addr = signer.address();
409        assert!(addr.starts_with('A'), "NEO address must start with 'A'");
410        assert_eq!(addr.len(), 34); // NEO N3 addresses are 34 chars
411        assert!(validate_address(&addr));
412    }
413
414    #[test]
415    fn test_neo_script_hash_length() {
416        let signer = NeoSigner::generate().unwrap();
417        let hash = signer.script_hash();
418        assert_eq!(hash.len(), 20);
419    }
420
421    #[test]
422    fn test_neo_address_validation_edges() {
423        assert!(!validate_address(""));
424        assert!(!validate_address("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); // Bitcoin
425        assert!(!validate_address("AINVALID"));
426    }
427
428    #[test]
429    fn test_neo_address_deterministic() {
430        let signer = NeoSigner::generate().unwrap();
431        let addr1 = signer.address();
432        let addr2 = signer.address();
433        assert_eq!(addr1, addr2);
434    }
435
436    // ─── WIF Tests ──────────────────────────────────────────────
437
438    #[test]
439    fn test_neo_wif_roundtrip() {
440        let signer = NeoSigner::generate().unwrap();
441        let wif = signer.to_wif();
442        let restored = NeoSigner::from_wif(&wif).unwrap();
443        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
444    }
445
446    #[test]
447    fn test_neo_wif_format() {
448        let signer = NeoSigner::generate().unwrap();
449        let wif = signer.to_wif();
450        // WIF starts with 'K' or 'L' for compressed keys
451        assert!(
452            wif.starts_with('K') || wif.starts_with('L'),
453            "WIF should start with K or L, got: {}",
454            &wif[..1]
455        );
456    }
457
458    #[test]
459    fn test_neo_wif_invalid_checksum() {
460        let signer = NeoSigner::generate().unwrap();
461        let wif = signer.to_wif();
462        // Tamper with the last character
463        let mut bad = wif.chars().collect::<Vec<_>>();
464        let last = bad.len() - 1;
465        bad[last] = if bad[last] == 'A' { 'B' } else { 'A' };
466        let bad_wif: String = bad.into_iter().collect();
467        assert!(NeoSigner::from_wif(&bad_wif).is_err());
468    }
469
470    #[test]
471    fn test_neo_wif_invalid_base58() {
472        assert!(NeoSigner::from_wif("0OIl").is_err()); // invalid Base58 chars
473    }
474
475    // ─── WIF Official Test Vector (neo.org) ─────────────────────
476
477    /// Known-good test vector from neo.org documentation:
478    /// private key: c7134d6fd8e73d819e82755c64c93788d8db0961929e025a53363c4cc02a6962
479    /// expected WIF: L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU
480    #[test]
481    fn test_neo_wif_known_vector() {
482        let privkey =
483            hex::decode("c7134d6fd8e73d819e82755c64c93788d8db0961929e025a53363c4cc02a6962")
484                .unwrap();
485        let signer = NeoSigner::from_bytes(&privkey).unwrap();
486        let wif = signer.to_wif();
487        assert_eq!(
488            wif.as_str(),
489            "L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU",
490            "WIF must match neo.org official test vector"
491        );
492
493        // Reverse: import WIF, verify same public key
494        let restored =
495            NeoSigner::from_wif("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU").unwrap();
496        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
497    }
498}