Skip to main content

Authorization

The authorization example demonstrates how to write a contract function that verifies an Identifiers signature before proceeding with the rest of the function. In this example, data is stored under an Identifier after authorization has been verified.

Run the Example

First go through the Setup process to get your development environment configured, then clone the v0.0.4 tag of soroban-examples repository:

git clone -b v0.0.4 https://github.com/stellar/soroban-examples

To run the tests for the example, navigate to the authorization directory, and use cargo test.

cd authorization
cargo test

You should see the output:

running 2 tests
test test::test ... ok
test test::bad_data - should panic ... ok

Dependencies

The authorization example uses the Soroban auth SDK, and has the following Soroban dependencies in its Cargo.toml file.

"authorization/src/Cargo.toml
[dependencies]
soroban-sdk = "0.0.4"
soroban-auth = "0.0.4"

[dev_dependencies]
soroban-sdk = { version = "0.0.4", features = ["testutils"] }
soroban-auth = { version = "0.0.4", features = ["testutils"] }

Code

authorization/src/lib.rs
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
SavedNum(Identifier),
Nonce(Identifier),
Admin,
}

fn read_nonce(e: &Env, id: Identifier) -> BigInt {
let key = DataKey::Nonce(id);
e.contract_data()
.get(key)
.unwrap_or_else(|| Ok(BigInt::zero(e)))
.unwrap()
}
struct NonceForSignature(Signature);

impl NonceAuth for NonceForSignature {
fn read_nonce(e: &Env, id: Identifier) -> BigInt {
read_nonce(e, id)
}

fn read_and_increment_nonce(&self, e: &Env, id: Identifier) -> BigInt {
let key = DataKey::Nonce(id.clone());
let nonce = Self::read_nonce(e, id);
e.contract_data().set(key, &nonce + 1);
nonce
}

fn signature(&self) -> &Signature {
&self.0
}
}

pub struct ExampleContract;

#[contractimpl]
impl ExampleContract {
/// Set the admin identifier. May be called only once.
pub fn set_admin(e: Env, admin: Identifier) {
if e.contract_data().has(DataKey::Admin) {
panic!("admin is already set")
}

e.contract_data().set(DataKey::Admin, admin);
}

/// Save the number for an authenticated [Identifier].
pub fn save_num(e: Env, sig: Signature, nonce: BigInt, num: BigInt) {
let auth_id = sig.get_identifier(&e);

check_auth(
&e,
&NonceForSignature(sig),
nonce.clone(),
symbol!("save_num"),
(&auth_id, nonce, &num).into_val(&e),
);

e.contract_data().set(DataKey::SavedNum(auth_id), num);
}

// The admin can write data for any Identifier
pub fn overwrite(e: Env, sig: Signature, nonce: BigInt, id: Identifier, num: BigInt) {
let auth_id = sig.get_identifier(&e);
if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() {
panic!("not authorized by admin")
}

check_auth(
&e,
&NonceForSignature(sig),
nonce.clone(),
symbol!("overwrite"),
(auth_id, nonce, &id, &num).into_val(&e),
);

e.contract_data().set(DataKey::SavedNum(id), num);
}

pub fn nonce(e: Env, id: Identifier) -> BigInt {
read_nonce(&e, id)
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/authorization

Authorization semantics

This section describes a specific implementation of the general principles discussed in authorization.

Identities

#[derive(Clone)]
#[contracttype]
pub enum Identifier {
Contract(BytesN<32>),
Ed25519(BytesN<32>),
Account(BytesN<32>),
}

The token contract understands three kinds of identities: contracts, Ed25519 public keys, and Stellar accounts. For each kind of identity, there is a corresponding authorization mechanism.

Contract authorization

A contract identity provides authorization simply by being the invoker of the token contract.

Ed25519 public key authorization

#[derive(Clone)]
#[contracttype(lib = "soroban_sdk_auth")]
pub struct Ed25519Signature {
pub public_key: BytesN<32>,
pub signature: BytesN<64>,
}

An Ed25519 public key identity can provide authorization by signing an appropriate message with the associated private key. The authorization is just the 64-byte signature, represented as BytesN<64> in the contract.

Stellar account authorization

#[derive(Clone)]
#[contracttype(lib = "soroban_sdk_auth")]
pub struct AccountSignatures {
pub account_id: BytesN<32>,
pub signatures: Vec<Ed25519Signature>,
}

A Stellar account identity can provide authorization by signing an appropriate message with the private keys associated with the signers of that Stellar account. The total signing weight of the signers must exceed the medium threshold of that Stellar account. The authorization is a vector, where each element of the vector contains an Ed25519Signature corresponding to the authorization provided by an account signer.

Payloads

#[derive(Clone)]
#[contracttype(lib = "soroban_sdk_auth")]
pub struct SignaturePayloadV0 {
pub function: Symbol,
pub contract: BytesN<32>,
pub network: Bytes,
pub args: Vec<RawVal>,
}

#[derive(Clone)]
#[contracttype(lib = "soroban_sdk_auth")]
pub enum SignaturePayload {
V0(SignaturePayloadV0),
}

Signatures are derived by signing the SignaturePayload enum, which has one value at the moment, V0. SignaturePayloadV0 uses the function, contract, and network to determine where the signature can be used and the args to provide additional information specific to that usage.

danger

SignaturePayloadV0 does not include any replay prevention by default. We recommend using nonce-based replay prevention and including the nonce in the args.

Replay prevention

Whenever signatures are used to permit an operation, there is a risk of "replay". Replay occurs when a single signature is used to permit an operation multiple times. Such a situation can be catastrophic. For example, imagine that you sign a message permitting 1 dollar to be sent to an acquantaince. If there were no replay prevention, then a malicious acquantaince could use that message to repeatedly transfer 1 dollar from you to them. In the end, your bank account would be empty.

Contracts can provide replay prevention by using a nonce. The payloads that are signed to provide authorization contain a nonce. The contract also stores a nonce (typically per identity). When checking signatures, the contract loads the nonce for the relevant identity. When an operation succeeds, the nonce stored in the contract is incremented. This makes it impossible to reuse a signature.

How it Works

Implement NonceAuth

NonceAuth is a trait in the soroban_sdk_auth crate that manages the nonce and wraps the Signature that the contract will try to verifiy. A struct that implements NonceAuth is expected by the check_auth sdk function. You can see below that we have a DataKey for the nonce tied to an Identifier, and this DataKey is used to manage the nonces for this contract.

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
SavedNum(Identifier),
Nonce(Identifier),
Admin,
}

fn read_nonce(e: &Env, id: Identifier) -> BigInt {
let key = DataKey::Nonce(id);
e.contract_data()
.get(key)
.unwrap_or_else(|| Ok(BigInt::zero(e)))
.unwrap()
}

struct NonceForSignature(Signature);

impl NonceAuth for NonceForSignature {
fn read_nonce(e: &Env, id: Identifier) -> BigInt {
read_nonce(e, id)
}

fn read_and_increment_nonce(&self, e: &Env, id: Identifier) -> BigInt {
let key = DataKey::Nonce(id.clone());
let nonce = Self::read_nonce(e, id);
e.contract_data().set(key, &nonce + 1);
nonce
}

fn signature(&self) -> &Signature {
&self.0
}
}

Check authorization in contract function

The save_num function stores a number in a DataKey::SavedNum tied to an Identifier with it's authorization.

The check_auth method in the SDK is used for signature verification, and here are the important authorization takeaways from the example below -

  1. The nonce is included in the list of parameters for the contract function.
  2. The Signature is passed into check_auth wrapped in NonceForSignature.
  3. The function parameter to check_auth is the name of the invoked function.
  4. The last argument passed to check_auth is a list of arguments that are expected in the signed payload. The interesting thing to note here is that it includes the Identifier from the sig and the nonce.
/// Save the number for an authenticated [Identifier].
pub fn save_num(e: Env, sig: Signature, nonce: BigInt, num: BigInt) {
let auth_id = sig.get_identifier(&e);

check_auth(
&e,
&NonceForSignature(sig),
nonce.clone(),
symbol!("save_num"),
(&auth_id, nonce, &num).into_val(&e),
);

e.contract_data().set(DataKey::SavedNum(auth_id), num);
}

Admin privileges

Some contracts may want to set an admin account that is allowed special privilege. The set_admin function here stores an Identifier as an admin, and that admin is the only one that can call overwrite.

// Sets the admin identifier
pub fn set_admin(e: Env, admin: Identifier) {
if e.contract_data().has(DataKey::Admin) {
panic!("admin is already set")
}

e.contract_data().set(DataKey::Admin, admin);
}

// The admin can write the number for any [Identifier]
pub fn overwrite(e: Env, sig: Signature, nonce: BigInt, id: Identifier, num: BigInt) {
let auth_id = sig.get_identifier(&e);
if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() {
panic!("not authorized by admin")
}

check_auth(
&e,
&NonceForSignature(sig),
nonce.clone(),
symbol!("overwrite"),
(auth_id, nonce, &id, &num).into_val(&e),
);

e.contract_data().set(DataKey::SavedNum(id), num);
}

Retrieving the Nonce

Users of this contract will need to know which nonce to use, so the contract exposes this information.

pub fn nonce(e: Env, to: Identifier) -> BigInt {
read_nonce(&e, to)
}