Skip to main content

aptos_sdk/account/
secp256r1.rs

1// Module docs prefer prose ("Aptos networks", "WebAuthn", etc.) over backticks
2// in several places where clippy's pedantic `doc_markdown` lint would otherwise
3// fire. We allow the lint locally to keep the deprecation note readable.
4#![allow(clippy::doc_markdown)]
5
6//! `Secp256r1` (P-256) account implementation.
7//!
8//! `Secp256r1`, also known as P-256 or `prime256v1`, is commonly used in
9//! WebAuthn / Passkey implementations.
10//!
11//! # ⚠️ Deprecated for transaction signing
12//!
13//! [`Secp256r1Account`] cannot successfully submit transactions on current
14//! Aptos networks. The on-chain `AnySignature` enum reserves variant index
15//! `2` for **WebAuthn** (a `PartialAuthenticatorAssertionResponse` that
16//! wraps a `secp256r1` signature in `authenticator_data` /
17//! `client_data_json`) -- not for bare `secp256r1` ECDSA signatures. A
18//! `Secp256r1Account`-signed transaction is therefore rejected by every
19//! Aptos validator with a deserialization-level error.
20//!
21//! Use [`WebAuthnAccount`](super::WebAuthnAccount) for any new code that
22//! needs to sign Aptos transactions with a P-256 key. `WebAuthnAccount`
23//! reuses [`Secp256r1PrivateKey`] / [`Secp256r1PublicKey`] internally but
24//! emits the correct on-chain wire format. See the on-chain definition in
25//! [aptos-core][webauthn-rs].
26//!
27//! `Secp256r1Account` remains available for off-chain uses (raw P-256
28//! `sign` / `verify`, key-management interop), but every API surface here
29//! that touches on-chain semantics is marked `#[deprecated]`.
30//!
31//! [webauthn-rs]: https://github.com/aptos-labs/aptos-core/blob/main/types/src/transaction/webauthn.rs
32
33use crate::account::account::{Account, AuthenticationKey};
34use crate::crypto::{
35    SINGLE_KEY_SCHEME, Secp256r1PrivateKey, Secp256r1PublicKey, derive_authentication_key,
36};
37use crate::error::AptosResult;
38use crate::types::AccountAddress;
39use std::fmt;
40
41/// A `Secp256r1` (P-256) ECDSA account.
42///
43/// # ⚠️ Deprecated for on-chain transaction signing
44///
45/// On current Aptos networks the on-chain `AnySignature` variant at index
46/// 2 is `WebAuthn`, **not** bare `secp256r1` ECDSA. Transactions signed by
47/// this account type are rejected by every Aptos validator. Use
48/// [`WebAuthnAccount`](super::WebAuthnAccount) instead -- it reuses the
49/// same key material and produces the correct WebAuthn-envelope wire
50/// format.
51///
52/// This type is still useful for off-chain P-256 use (sign / verify of
53/// arbitrary bytes, key import/export, key-derivation testing) and for
54/// constructing [`MultiKeyAccount`](super::MultiKeyAccount) public-key
55/// material.
56///
57/// # Example
58///
59/// ```rust
60/// # #![allow(deprecated)]
61/// use aptos_sdk::account::Secp256r1Account;
62///
63/// // For off-chain signing only -- this account cannot submit
64/// // transactions to a live Aptos network. Use `WebAuthnAccount` for that.
65/// let account = Secp256r1Account::generate();
66/// println!("Address: {}", account.address());
67/// ```
68#[deprecated(
69    since = "0.5.0",
70    note = "Use `WebAuthnAccount` for on-chain transaction signing. Bare \
71            secp256r1 signatures are not accepted by Aptos validators (the \
72            on-chain AnySignature variant 2 is WebAuthn, not Secp256r1Ecdsa). \
73            This type is retained for off-chain use only."
74)]
75#[derive(Clone)]
76pub struct Secp256r1Account {
77    private_key: Secp256r1PrivateKey,
78    public_key: Secp256r1PublicKey,
79    address: AccountAddress,
80}
81
82#[allow(deprecated)]
83impl Secp256r1Account {
84    /// Generates a new random Secp256r1 account.
85    pub fn generate() -> Self {
86        let private_key = Secp256r1PrivateKey::generate();
87        Self::from_private_key(private_key)
88    }
89
90    /// Creates an account from a private key.
91    pub fn from_private_key(private_key: Secp256r1PrivateKey) -> Self {
92        let public_key = private_key.public_key();
93        let address = public_key.to_address();
94        Self {
95            private_key,
96            public_key,
97            address,
98        }
99    }
100
101    /// Creates an account from private key bytes.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the bytes are not a valid Secp256r1 private key (must be exactly 32 bytes and a valid curve point).
106    pub fn from_private_key_bytes(bytes: &[u8]) -> AptosResult<Self> {
107        let private_key = Secp256r1PrivateKey::from_bytes(bytes)?;
108        Ok(Self::from_private_key(private_key))
109    }
110
111    /// Creates an account from a private key hex string.
112    ///
113    /// # Errors
114    ///
115    /// This function will return an error if:
116    /// - The hex string is invalid or cannot be decoded
117    /// - The decoded bytes are not a valid Secp256r1 private key
118    pub fn from_private_key_hex(hex_str: &str) -> AptosResult<Self> {
119        let private_key = Secp256r1PrivateKey::from_hex(hex_str)?;
120        Ok(Self::from_private_key(private_key))
121    }
122
123    /// Returns the account address.
124    pub fn address(&self) -> AccountAddress {
125        self.address
126    }
127
128    /// Returns the public key.
129    pub fn public_key(&self) -> &Secp256r1PublicKey {
130        &self.public_key
131    }
132
133    /// Returns a reference to the private key.
134    pub fn private_key(&self) -> &Secp256r1PrivateKey {
135        &self.private_key
136    }
137
138    /// Signs a message and returns the Secp256r1 signature.
139    pub fn sign_message(&self, message: &[u8]) -> crate::crypto::Secp256r1Signature {
140        self.private_key.sign(message)
141    }
142}
143
144#[allow(deprecated)]
145impl Account for Secp256r1Account {
146    fn address(&self) -> AccountAddress {
147        self.address
148    }
149
150    fn authentication_key(&self) -> AuthenticationKey {
151        let uncompressed = self.public_key.to_uncompressed_bytes();
152        let mut bcs_bytes = Vec::with_capacity(1 + 1 + uncompressed.len());
153        bcs_bytes.push(0x02); // Secp256r1Ecdsa variant
154        bcs_bytes.push(65); // ULEB128(65)
155        bcs_bytes.extend_from_slice(&uncompressed);
156        let key = derive_authentication_key(&bcs_bytes, SINGLE_KEY_SCHEME);
157        AuthenticationKey::new(key)
158    }
159
160    fn sign(&self, message: &[u8]) -> crate::error::AptosResult<Vec<u8>> {
161        // Return BCS-serialized `AnySignature::Secp256r1` (variant=2, len=64, bytes).
162        //
163        // NOTE: Raw `AnySignature::Secp256r1Ecdsa` is NOT honored as a
164        // single-key transaction authenticator -- the on-chain variant 2 in
165        // `AnySignature` is the `WebAuthn` wrapper, which carries an
166        // `AssertionSignature` plus a `client_data_json` and authenticator
167        // data, not a bare ECDSA signature. This signing path produces a wire
168        // format consistent with the SDK's address-derivation, but submitting
169        // such transactions on-chain fails at signature verification. For
170        // on-chain P-256 signing use [`WebAuthnAccount`](super::WebAuthnAccount),
171        // which already exists and emits the correct WebAuthn-envelope wire
172        // format; `Ed25519SingleKeyAccount` or `Secp256k1Account` also work for
173        // end-to-end transaction flows.
174        let sig = self.private_key.sign(message).to_bytes().to_vec();
175        debug_assert_eq!(
176            sig.len(),
177            64,
178            "Secp256r1 signature must be exactly 64 bytes (R || S)"
179        );
180        let mut out = Vec::with_capacity(1 + 1 + sig.len());
181        out.push(0x02); // AnySignature::Secp256r1 variant
182        out.push(64); // ULEB128(64)
183        out.extend_from_slice(&sig);
184        Ok(out)
185    }
186
187    fn public_key_bytes(&self) -> Vec<u8> {
188        // BCS-serialized `AnyPublicKey::Secp256r1Ecdsa`
189        // (variant=2, ULEB128(65), 65 bytes SEC1 uncompressed).
190        let uncompressed = self.public_key.to_uncompressed_bytes();
191        let mut out = Vec::with_capacity(1 + 1 + uncompressed.len());
192        out.push(0x02); // AnyPublicKey::Secp256r1Ecdsa variant
193        out.push(65); // ULEB128(65)
194        out.extend_from_slice(&uncompressed);
195        out
196    }
197
198    fn signature_scheme(&self) -> u8 {
199        SINGLE_KEY_SCHEME
200    }
201}
202
203#[allow(deprecated)]
204impl fmt::Debug for Secp256r1Account {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("Secp256r1Account")
207            .field("address", &self.address)
208            .field("public_key", &self.public_key)
209            .finish_non_exhaustive()
210    }
211}
212
213#[cfg(test)]
214#[allow(deprecated)] // we are deliberately testing the deprecated API
215mod tests {
216    use super::*;
217    use crate::account::Account;
218
219    #[test]
220    fn test_generate() {
221        let account = Secp256r1Account::generate();
222        assert!(!account.address().is_zero());
223    }
224
225    #[test]
226    fn test_from_private_key_roundtrip() {
227        let account = Secp256r1Account::generate();
228        let bytes = account.private_key().to_bytes();
229
230        let restored = Secp256r1Account::from_private_key_bytes(&bytes).unwrap();
231        assert_eq!(account.address(), restored.address());
232    }
233
234    #[test]
235    fn test_sign_and_verify() {
236        let account = Secp256r1Account::generate();
237        let message = b"hello world";
238
239        let signature = account.sign_message(message);
240        assert!(account.public_key().verify(message, &signature).is_ok());
241    }
242
243    #[test]
244    fn test_account_trait() {
245        let account = Secp256r1Account::generate();
246        let message = b"test message";
247
248        // sign() returns BCS(AnySignature::Secp256r1) = 1 + 1 + 64 = 66 bytes.
249        let sig_bytes = account.sign(message).unwrap();
250        assert_eq!(sig_bytes.len(), 66);
251        assert_eq!(sig_bytes[0], 0x02, "AnySignature::Secp256r1 variant tag");
252        assert_eq!(sig_bytes[1], 64, "ULEB128(64)");
253
254        // public_key_bytes() returns BCS(AnyPublicKey::Secp256r1Ecdsa) =
255        // variant(2) + ULEB128(65) + 65-byte SEC1 uncompressed. Total = 67 bytes.
256        let pub_key_bytes = account.public_key_bytes();
257        assert_eq!(pub_key_bytes.len(), 67);
258        assert_eq!(
259            pub_key_bytes[0], 0x02,
260            "AnyPublicKey::Secp256r1Ecdsa variant tag"
261        );
262        assert_eq!(pub_key_bytes[1], 65, "ULEB128(65)");
263        assert_eq!(pub_key_bytes[2], 0x04, "SEC1 uncompressed marker");
264
265        assert!(!account.authentication_key().as_bytes().is_empty());
266    }
267
268    #[test]
269    fn test_from_private_key() {
270        let original = Secp256r1Account::generate();
271        let private_key = original.private_key().clone();
272        let restored = Secp256r1Account::from_private_key(private_key);
273        assert_eq!(original.address(), restored.address());
274    }
275
276    #[test]
277    fn test_from_private_key_hex() {
278        let original = Secp256r1Account::generate();
279        let hex = original.private_key().to_hex();
280        let restored = Secp256r1Account::from_private_key_hex(&hex).unwrap();
281        assert_eq!(original.address(), restored.address());
282    }
283
284    #[test]
285    fn test_signature_scheme() {
286        let account = Secp256r1Account::generate();
287        assert_eq!(account.signature_scheme(), SINGLE_KEY_SCHEME);
288    }
289
290    #[test]
291    fn test_debug_output() {
292        let account = Secp256r1Account::generate();
293        let debug = format!("{account:?}");
294        assert!(debug.contains("Secp256r1Account"));
295        assert!(debug.contains("address"));
296    }
297
298    #[test]
299    fn test_invalid_private_key_bytes() {
300        let result = Secp256r1Account::from_private_key_bytes(&[0u8; 16]);
301        assert!(result.is_err());
302    }
303
304    #[test]
305    fn test_invalid_private_key_hex() {
306        let result = Secp256r1Account::from_private_key_hex("invalid");
307        assert!(result.is_err());
308    }
309}