Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account Abstraction - Custom Validation Signature Scheme #229

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ dependencies = [
"snforge_std",
]

[[package]]
name = "custom_signature_validation"
version = "0.1.0"

[[package]]
name = "custom_type_serde"
version = "0.1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
14 changes: 14 additions & 0 deletions listings/advanced-concepts/custom_signature_validation/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "custom_signature_validation"
version.workspace = true
edition = "2023_11"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet.workspace = true

[scripts]
test.workspace = true

[[target.starknet-contract]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// ANCHOR: custom_signature_scheme
use starknet::secp256_trait::{
Secp256PointTrait, Signature as Secp256Signature, recover_public_key, is_signature_entry_valid
};
use starknet::secp256r1::Secp256r1Point;
use starknet::secp256k1::Secp256k1Point;
use starknet::{EthAddress, eth_signature::is_eth_signature_valid};
use core::traits::TryInto;


const SECP256R1_SIGNER_TYPE: felt252 = 'Secp256r1 Signer';
const SECP256K1_SIGNER_TYPE: felt252 = 'Secp256k1 Signer';
const SECP_256_R1_HALF: u256 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
/ 2;
const SECP_256_K1_HALF: u256 = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
/ 2;


#[derive(Drop, Copy, PartialEq, Serde, Default)]
enum SignerType {
#[default]
Secp256r1,
Secp256k1,
}

#[derive(Drop, Copy, Serde)]
enum SignerSignature {
Secp256r1: (Secp256r1Signer, Secp256Signature),
Secp256k1: (Secp256k1Signer, Secp256Signature),
}

#[derive(Drop, Copy, Serde)]
enum Signer {
Secp256r1: Secp256r1Signer,
Secp256k1: Secp256k1Signer,
}

#[derive(Drop, Copy, Serde, PartialEq)]
struct Secp256r1Signer {
pubkey: NonZero<u256>
}

#[derive(Drop, Copy, PartialEq)]
struct Secp256k1Signer {
pubkey_hash: EthAddress
}


// To ensure the pubkey hash is not zero
impl Secp256k1SignerSerde of Serde<Secp256k1Signer> {
#[inline(always)]
fn serialize(self: @Secp256k1Signer, ref output: Array<felt252>) {
self.pubkey_hash.serialize(ref output);
}

#[inline(always)]
fn deserialize(ref serialized: Span<felt252>) -> Option<Secp256k1Signer> {
let pubkey_hash = Serde::<EthAddress>::deserialize(ref serialized)?;
assert(pubkey_hash.address != 0, 'zero pub key hash');
Option::Some(Secp256k1Signer { pubkey_hash })
}
}

// ANCHOR: is_valid_signature
///@notice Check if secp256k1 and secp256r1 signatures are valid
trait Secp256SignatureTrait {
fn is_valid_signature(self: SignerSignature, hash: felt252) -> bool;
fn signer(self: SignerSignature) -> Signer;
}

///@notice Check if secp256k1 and secp256r1 signatures are valid
impl Secp256SignatureImpl of Secp256SignatureTrait {
#[inline(always)]
fn is_valid_signature(self: SignerSignature, hash: felt252) -> bool {
match self {
SignerSignature::Secp256r1((
signer, signature
)) => is_valid_secp256r1_signature(hash.into(), signer, signature),
SignerSignature::Secp256k1((
signer, signature
)) => is_valid_secp256k1_signature(hash.into(), signer.pubkey_hash.into(), signature),
}
}


#[inline(always)]
fn signer(self: SignerSignature) -> Signer {
match self {
SignerSignature::Secp256k1((signer, _)) => Signer::Secp256k1(signer),
SignerSignature::Secp256r1((signer, _)) => Signer::Secp256r1(signer),
}
}
}

///@notice To validate secp256k1 signature
#[inline(always)]
fn is_valid_secp256k1_signature(
hash: u256, pubkey_hash: EthAddress, signature: Secp256Signature
) -> bool {
assert(signature.s <= SECP_256_K1_HALF, 'malleable signature');
is_eth_signature_valid(hash, signature, pubkey_hash).is_ok()
}

///@notice To validate secp256r1 signature
#[inline(always)]
fn is_valid_secp256r1_signature(
hash: u256, signer: Secp256r1Signer, signature: Secp256Signature
) -> bool {
assert(is_signature_entry_valid::<Secp256r1Point>(signature.s), 'invalid s-value');
assert(is_signature_entry_valid::<Secp256r1Point>(signature.r), 'invalid r-value');
assert(signature.s <= SECP_256_R1_HALF, 'malleable signature');
let recovered_pubkey = recover_public_key::<Secp256r1Point>(hash, signature)
.expect('invalid sign format');
let (recovered_signer, _) = recovered_pubkey.get_coordinates().expect('invalid sig format');
recovered_signer == signer.pubkey.into()
}
// ANCHOR_END: is_valid_signature

// impl to convert signer type into felt252 using into()
impl SignerTypeIntoFelt252 of Into<SignerType, felt252> {
#[inline(always)]
fn into(self: SignerType) -> felt252 {
match self {
SignerType::Secp256k1 => 1,
SignerType::Secp256r1 => 2,
}
}
}


// impl to convert u256 type into SignerType using try_into()
impl U256TryIntoSignerType of TryInto<u256, SignerType> {
#[inline(always)]
fn try_into(self: u256) -> Option<SignerType> {
if self == 1 {
Option::Some(SignerType::Secp256k1)
} else if self == 2 {
Option::Some(SignerType::Secp256r1)
} else {
Option::None
}
}
}
// ANCHOR_END: custom_signature_scheme

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod custom_signature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Custom Signature Validation Scheme

Digital signatures are a fundamental aspect of modern cryptography used to verify the authenticity and integrity of digital messages or transactions. They are based on public-key cryptography, where a pair of keys (a public key and a private key) are used to create and verify signatures.

Private keys are kept secret and secured by the owner. They are used to sign data such as messages or transactions, which can be verified by anyone with the public key.

Account Abstraction is a native feature on Starknet, this makes it possible for anyone to implement custom signature schemes and use it to validate transactions with the implementation of the signature validation logic.

### The Concepts of Accounts and Signers

i. **Account:** All accounts are smart contracts that can hold assets and execute transactions on Starknet, these account contracts however must implement some specific methods outlined in [SNIP-6.](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md)

ii. **Signers:** These are responsible for digitally signing transactions and also provide the authorization needed to initiate transactions. Transaction signing is done offchain on Starknet.

Digital signatures are cryptographic proofs that transactions are authorized by corresponding accounts.

### Signature validation on Starknet

On Starknet transactions are signed offchain, which means that the signature process happens outside the blockchain. The signed transaction is then submitted to Starknet network for verification and execution. Read more: [Starknet-js docs](https://www.starknetjs.com/docs/guides/signature/)

All Account contracts on Starknet must implement the SNIP-6 standard as already stated. The methods outlined in the standard provide means to move offchain signatures onchain and execute them. More details below:

```cairo
// [!include ~/listings/advanced-concepts/simple_account/src/simple_account.cairo]
```

On Ethereum, only **one** signature scheme is used: ECDSA. It makes Ethereum more secure but less flexible.
Custom signature validation used on Starknet gives room for more flexibility, however, care must be taken to validate all signatures meticulously to ensure that:

a. the message has not been altered.
b. the signer owns the private key corresponding to the public key.


### Custom signature validation sample

The example below shows a sample validation of `Secp256r1` and `Secp256k1` signature schemes:

```cairo
// [!include ~/listings/advanced-concepts/custom_signature_validation/src/custom_signature.cairo:is_valid_signature]
```