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
155        let checksum = crypto::double_sha256(&decoded[..34]);
156        if decoded[34..38] != checksum[..4] {
157            return Err(SignerError::InvalidPrivateKey("WIF: bad checksum".into()));
158        }
159
160        use crate::traits::KeyPair;
161        Self::from_bytes(&decoded[1..33])
162    }
163}
164
165impl Drop for NeoSigner {
166    fn drop(&mut self) {
167        // p256::SigningKey implements ZeroizeOnDrop internally
168    }
169}
170
171impl NeoSigner {
172    pub(crate) fn sign_digest(&self, digest: &[u8; 32]) -> Result<NeoSignature, SignerError> {
173        let sig: P256Signature = self
174            .signing_key
175            .sign_prehash(digest)
176            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
177        let mut bytes = [0u8; 64];
178        bytes.copy_from_slice(&sig.to_bytes());
179        Ok(NeoSignature { bytes })
180    }
181}
182
183impl traits::Signer for NeoSigner {
184    type Signature = NeoSignature;
185    type Error = SignerError;
186
187    fn sign(&self, message: &[u8]) -> Result<NeoSignature, SignerError> {
188        let hash = crypto::sha256(message);
189        self.sign_digest(&hash)
190    }
191
192    fn sign_prehashed(&self, digest: &[u8]) -> Result<NeoSignature, SignerError> {
193        if digest.len() != 32 {
194            return Err(SignerError::InvalidHashLength {
195                expected: 32,
196                got: digest.len(),
197            });
198        }
199        let mut hash = [0u8; 32];
200        hash.copy_from_slice(digest);
201        self.sign_digest(&hash)
202    }
203
204    fn public_key_bytes(&self) -> Vec<u8> {
205        self.signing_key
206            .verifying_key()
207            .to_encoded_point(true)
208            .as_bytes()
209            .to_vec()
210    }
211
212    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
213        self.signing_key
214            .verifying_key()
215            .to_encoded_point(false)
216            .as_bytes()
217            .to_vec()
218    }
219}
220
221impl traits::KeyPair for NeoSigner {
222    fn generate() -> Result<Self, SignerError> {
223        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
224        crate::security::secure_random(&mut *key_bytes)?;
225        let signing_key = SigningKey::from_bytes((&*key_bytes).into())
226            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
227        Ok(Self { signing_key })
228    }
229
230    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
231        if private_key.len() != 32 {
232            return Err(SignerError::InvalidPrivateKey(format!(
233                "expected 32 bytes, got {}",
234                private_key.len()
235            )));
236        }
237        let signing_key = SigningKey::from_bytes(private_key.into())
238            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
239        Ok(Self { signing_key })
240    }
241
242    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
243        Zeroizing::new(self.signing_key.to_bytes().to_vec())
244    }
245}
246
247/// NEO ECDSA verifier (P-256).
248pub struct NeoVerifier {
249    verifying_key: VerifyingKey,
250}
251
252impl NeoVerifier {
253    /// Create from SEC1 public key bytes.
254    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
255        let verifying_key = VerifyingKey::from_sec1_bytes(bytes)
256            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
257        Ok(Self { verifying_key })
258    }
259
260    fn verify_digest(
261        &self,
262        digest: &[u8; 32],
263        signature: &NeoSignature,
264    ) -> Result<bool, SignerError> {
265        let sig = P256Signature::from_bytes((&signature.bytes).into())
266            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
267        match self.verifying_key.verify_prehash(digest, &sig) {
268            Ok(()) => Ok(true),
269            Err(_) => Ok(false),
270        }
271    }
272}
273
274impl traits::Verifier for NeoVerifier {
275    type Signature = NeoSignature;
276    type Error = SignerError;
277
278    fn verify(&self, message: &[u8], signature: &NeoSignature) -> Result<bool, SignerError> {
279        let hash = crypto::sha256(message);
280        self.verify_digest(&hash, signature)
281    }
282
283    fn verify_prehashed(
284        &self,
285        digest: &[u8],
286        signature: &NeoSignature,
287    ) -> Result<bool, SignerError> {
288        if digest.len() != 32 {
289            return Err(SignerError::InvalidHashLength {
290                expected: 32,
291                got: digest.len(),
292            });
293        }
294        let mut hash = [0u8; 32];
295        hash.copy_from_slice(digest);
296        self.verify_digest(&hash, signature)
297    }
298}
299
300#[cfg(test)]
301#[allow(clippy::unwrap_used, clippy::expect_used)]
302mod tests {
303    use super::*;
304    use crate::traits::{KeyPair, Signer, Verifier};
305
306    #[test]
307    fn test_generate_keypair() {
308        let signer = NeoSigner::generate().unwrap();
309        let pubkey = signer.public_key_bytes();
310        assert_eq!(pubkey.len(), 33); // compressed P-256
311    }
312
313    #[test]
314    fn test_from_bytes_roundtrip() {
315        let signer = NeoSigner::generate().unwrap();
316        let restored = NeoSigner::from_bytes(&signer.private_key_bytes()).unwrap();
317        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
318    }
319
320    #[test]
321    fn test_sign_verify_roundtrip() {
322        let signer = NeoSigner::generate().unwrap();
323        let sig = signer.sign(b"hello neo").unwrap();
324        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
325        assert!(verifier.verify(b"hello neo", &sig).unwrap());
326    }
327
328    #[test]
329    fn test_p256_not_k256() {
330        // P-256 pubkeys are different from secp256k1 for the same private key bytes
331        let privkey =
332            hex::decode("708309a7449e156b0db70e5b52e606c7e094ed676ce8953bf6c14757c826f590")
333                .unwrap();
334        let neo_signer = NeoSigner::from_bytes(&privkey).unwrap();
335        let neo_pubkey = neo_signer.public_key_bytes();
336        // This is a P-256 pubkey, not secp256k1
337        assert_eq!(neo_pubkey.len(), 33);
338    }
339
340    #[test]
341    fn test_neo_serialization() {
342        let signer = NeoSigner::generate().unwrap();
343        let sig = signer.sign(b"serialization test").unwrap();
344        assert_eq!(sig.bytes.len(), 64); // r || s
345    }
346
347    // FIPS 186-4 / NIST CAVP P-256 Test Vector
348    #[test]
349    fn test_known_vector_p256_fips() {
350        let privkey =
351            hex::decode("708309a7449e156b0db70e5b52e606c7e094ed676ce8953bf6c14757c826f590")
352                .unwrap();
353        let signer = NeoSigner::from_bytes(&privkey).unwrap();
354        // Sign a test message and verify round-trip
355        let sig = signer.sign(b"NIST P-256 test").unwrap();
356        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
357        assert!(verifier.verify(b"NIST P-256 test", &sig).unwrap());
358    }
359
360    #[test]
361    fn test_invalid_privkey_rejected() {
362        assert!(NeoSigner::from_bytes(&[0u8; 32]).is_err());
363        assert!(NeoSigner::from_bytes(&[1u8; 31]).is_err());
364    }
365
366    #[test]
367    fn test_tampered_sig_fails() {
368        let signer = NeoSigner::generate().unwrap();
369        let sig = signer.sign(b"tamper").unwrap();
370        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
371        let mut tampered = sig.clone();
372        tampered.bytes[0] ^= 0xff;
373        let result = verifier.verify(b"tamper", &tampered);
374        assert!(result.is_err() || !result.unwrap());
375    }
376
377    #[test]
378    fn test_sign_prehashed_roundtrip() {
379        let signer = NeoSigner::generate().unwrap();
380        let msg = b"prehash neo";
381        let digest = crate::crypto::sha256(msg);
382        let sig = signer.sign_prehashed(&digest).unwrap();
383        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
384        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
385    }
386
387    #[test]
388    fn test_zeroize_on_drop() {
389        let signer = NeoSigner::generate().unwrap();
390        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
391        drop(signer);
392    }
393
394    #[test]
395    fn test_empty_message() {
396        let signer = NeoSigner::generate().unwrap();
397        let sig = signer.sign(b"").unwrap();
398        let verifier = NeoVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
399        assert!(verifier.verify(b"", &sig).unwrap());
400    }
401
402    // ─── NEO Known Address Vectors ──────────────────────────────
403
404    #[test]
405    fn test_neo_address_format() {
406        let signer = NeoSigner::generate().unwrap();
407        let addr = signer.address();
408        assert!(addr.starts_with('A'), "NEO address must start with 'A'");
409        assert_eq!(addr.len(), 34); // NEO N3 addresses are 34 chars
410        assert!(validate_address(&addr));
411    }
412
413    #[test]
414    fn test_neo_script_hash_length() {
415        let signer = NeoSigner::generate().unwrap();
416        let hash = signer.script_hash();
417        assert_eq!(hash.len(), 20);
418    }
419
420    #[test]
421    fn test_neo_address_validation_edges() {
422        assert!(!validate_address(""));
423        assert!(!validate_address("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); // Bitcoin
424        assert!(!validate_address("AINVALID"));
425    }
426
427    #[test]
428    fn test_neo_address_deterministic() {
429        let signer = NeoSigner::generate().unwrap();
430        let addr1 = signer.address();
431        let addr2 = signer.address();
432        assert_eq!(addr1, addr2);
433    }
434
435    // ─── WIF Tests ──────────────────────────────────────────────
436
437    #[test]
438    fn test_neo_wif_roundtrip() {
439        let signer = NeoSigner::generate().unwrap();
440        let wif = signer.to_wif();
441        let restored = NeoSigner::from_wif(&wif).unwrap();
442        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
443    }
444
445    #[test]
446    fn test_neo_wif_format() {
447        let signer = NeoSigner::generate().unwrap();
448        let wif = signer.to_wif();
449        // WIF starts with 'K' or 'L' for compressed keys
450        assert!(
451            wif.starts_with('K') || wif.starts_with('L'),
452            "WIF should start with K or L, got: {}",
453            &wif[..1]
454        );
455    }
456
457    #[test]
458    fn test_neo_wif_invalid_checksum() {
459        let signer = NeoSigner::generate().unwrap();
460        let wif = signer.to_wif();
461        // Tamper with the last character
462        let mut bad = wif.chars().collect::<Vec<_>>();
463        let last = bad.len() - 1;
464        bad[last] = if bad[last] == 'A' { 'B' } else { 'A' };
465        let bad_wif: String = bad.into_iter().collect();
466        assert!(NeoSigner::from_wif(&bad_wif).is_err());
467    }
468
469    #[test]
470    fn test_neo_wif_invalid_base58() {
471        assert!(NeoSigner::from_wif("0OIl").is_err()); // invalid Base58 chars
472    }
473
474    // ─── WIF Official Test Vector (neo.org) ─────────────────────
475
476    /// Known-good test vector from neo.org documentation:
477    /// private key: c7134d6fd8e73d819e82755c64c93788d8db0961929e025a53363c4cc02a6962
478    /// expected WIF: L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU
479    #[test]
480    fn test_neo_wif_known_vector() {
481        let privkey =
482            hex::decode("c7134d6fd8e73d819e82755c64c93788d8db0961929e025a53363c4cc02a6962")
483                .unwrap();
484        let signer = NeoSigner::from_bytes(&privkey).unwrap();
485        let wif = signer.to_wif();
486        assert_eq!(
487            wif.as_str(),
488            "L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU",
489            "WIF must match neo.org official test vector"
490        );
491
492        // Reverse: import WIF, verify same public key
493        let restored =
494            NeoSigner::from_wif("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU").unwrap();
495        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
496    }
497}