Secp256r1 (also known as P-256) is a widely adopted elliptic curve for public-key cryptography, supported by most modern cryptographic libraries and hardware. It is defined by parameters: prime p, coefficients a and b, base point G, and its prime order n. Unlike native Ed25519, secp256r1 requires a precompiled program for signature verification.

Why use Secp256r1 with Swig?

  • Native support for passkeys and hardware-backed authentication
  • Secure, standards-based cryptography
  • Compatible with modern browsers and devices
  • Enables session-based authorities for limited-lifetime delegation

Technical Overview

Secp256r1 for SWIG Wallet: Secp256r1 Authority: Represents an account or role that can sign transactions using a Secp256r1 key. Session Authority: Supports session-based authorities for limited-lifetime delegation. Signature Odometer: Each authority tracks a counter to prevent replay attacks.

How to Use Secp256r1 Authority

The walkthrough covers both Rust and TypeScript implementations:
  1. Key Generation
  2. Authority Management
  3. Transaction Signing & Replay Protection
  4. Session Authorities

TypeScript Implementation

Key Generation & Basic Usage

Here’s a complete TypeScript example using secp256r1 keys with Swig Wallet:
import { p256 } from '@noble/curves/nist';
import { Keypair, SystemProgram, Transaction } from '@solana/web3.js';
import { LiteSVM } from 'litesvm';
import {
  createSecp256r1AuthorityInfo,
  getSigningFnForSecp256r1PrivateKey,
  findSwigPda,
  getCreateSwigInstruction,
  getSignInstructions,
  Swig,
  Actions,
} from '@swig-wallet/classic';

async function setupSecp256r1Transfer() {
  // 1. Initialize test environment
  const svm = new LiteSVM();
  
  // 2. Generate secp256r1 keypair
  const userWallet = p256.keygen();
  const publicKey = p256.getPublicKey(userWallet.secretKey, true); // compressed
  
  // 3. Generate Solana keypair for paying fees
  const userKeypair = Keypair.generate();
  const recipientKeypair = Keypair.generate();
  
  // Fund the payer
  svm.airdrop(userKeypair.publicKey, 1000000000); // 1 SOL
  
  // 4. Create Swig authority info
  const authorityInfo = createSecp256r1AuthorityInfo(publicKey);
  
  // 5. Find Swig PDA
  const swigId = new Uint8Array(32);
  const [swigAddress] = findSwigPda(swigId);
  
  // 6. Create Swig account
  const createSwigInstruction = await getCreateSwigInstruction({
    authorityInfo,
    id: swigId,
    payer: userKeypair.publicKey,
    actions: Actions.set().all().get(),
  });
  
  // Send creation transaction
  const createTx = new Transaction().add(createSwigInstruction);
  svm.sendTransaction(createTx, [userKeypair]);
  
  // 7. Create signing function
  const signingFn = getSigningFnForSecp256r1PrivateKey(userWallet.secretKey);
  
  // 8. Load the created Swig
  const swig = await Swig.fromAccountAddress(svm, swigAddress);
  
  // 9. Create transfer instruction
  const transferInstruction = SystemProgram.transfer({
    fromPubkey: swigAddress,
    toPubkey: recipientKeypair.publicKey,
    lamports: 1000000, // 0.001 SOL
  });
  
  // 10. Sign the transfer using secp256r1
  const signedInstructions = await getSignInstructions(
    swig,
    0, // role ID
    [transferInstruction],
    false,
    {
      currentSlot: svm.getClock().slot,
      signingFn,
      payer: userKeypair.publicKey,
    }
  );
  
  // 11. Execute the signed transaction
  const signedTx = new Transaction().add(...signedInstructions);
  const result = svm.sendTransaction(signedTx, [userKeypair]);
  
  console.log('Transfer completed:', result);
  
  return {
    swigAddress,
    userWallet,
    signingFn,
    swig
  };
}

WebAuthn Passkey Integration

For browser-based applications, you can integrate WebAuthn passkeys:
import { PasskeyManager } from './helpers/passkey';
import {
  createSecp256r1AuthorityInfo,
  getSecp256r1WebAuthnSigningFn,
} from '@swig-wallet/classic';

async function setupPasskeyAuth() {
  // Check WebAuthn support
  if (!PasskeyManager.isSupported()) {
    throw new Error('WebAuthn not supported in this browser');
  }
  
  // Create or retrieve passkey
  let credential = PasskeyManager.getStoredCredential();
  if (!credential) {
    credential = await PasskeyManager.createPasskey('user@example.com');
  }
  
  // Create authority info from passkey
  const authorityInfo = createSecp256r1AuthorityInfo(credential.publicKey);
  
  // Create WebAuthn signing function
  const signingFn = getSecp256r1WebAuthnSigningFn({
    allowCredentials: [{
      id: credential.rawId,
      type: 'public-key',
    }],
    timeout: 60000,
    userVerification: 'preferred',
  });
  
  return { authorityInfo, signingFn };
}

Rust Implementation

A. Key Generation:

To use Secp256r1, a valid key pair is needed. A create_secp256r1_keypair function handles this using the openssl crate.
  • Curve: It uses the Nid::X9_62_PRIME256V1 curve, which is the standard for P-256.
  • Public Key Format: The public key is serialized into a 33-byte compressed format. This is crucial, as it’s the format the on-chain program expects.
/// Helper to generate a real secp256r1 key pair for testing
fn create_test_secp256r1_keypair() -> (openssl::ec::EcKey<openssl::pkey::Private>, [u8; 33]) {
    use openssl::{
        bn::BigNumContext,
        ec::{EcGroup, EcKey, PointConversionForm},
        nid::Nid,
    };

    // 1. Select the P-256 curve
    let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();

    // 2. Generate the private/public key pair
    let signing_key = EcKey::generate(&group).unwrap();
    let mut ctx = BigNumContext::new().unwrap();

    // 3. Serialize the public key to its 33-byte compressed format
    let pubkey_bytes = signing_key
        .public_key()
        .to_bytes(&group, PointConversionForm::COMPRESSED, &mut ctx)
        .unwrap();
    let pubkey_array: [u8; 33] = pubkey_bytes.try_into().unwrap();

    (signing_key, pubkey_array)
}

B. Authority Management:

A SWIG account can be created with a secp256r1 key as its primary authority or have one added later. This gives the secp256r1 key holder control over the account. For creating an Account with a secp256r1 Authority, the create_swig_secp256r1 function shows how to initialize a new swig account where a secp256r1 public key is the owner.
/// Helper function to create a swig account with secp256r1 authority for testing
fn create_swig_secp256r1(
    context: &mut SwigTestContext,
    public_key: &[u8; 33],
    id: [u8; 32],
) -> Result<(solana_sdk::pubkey::Pubkey, u8), Box<dyn std::error::Error>> {
    // To use find program address to get the swig address and bump

    // The instruction specifies the AuthorityType and the public key
    let create_ix = swig_interface::CreateInstruction::new(
        swig_address,
        swig_bump,
        payer_pubkey,
        AuthorityConfig {
            authority_type: AuthorityType::Secp256r1,
            authority: public_key,
        },
        vec![ClientAction::All(All {})], // Granting full permissions
        id,
    )?;

    // To create instruction using swig_interface::CreateInstruction
    // To create message using v0::Message::try_compile
    // To send transaction using context.svm.send_transaction

    Ok((swig_address, swig_bump))
}
A new secp256r1 authority can be added to an existing account. This operation must be signed by a current authority. The test test_secp256r1_add_authority_with_secp256r1 demonstrates a secp256r1 key authorizing the addition of another secp256r1 key.
#[test_log::test]
fn test_secp256r1_add_authority_with_secp256r1() {
    // Setup test context

    // The primary authority is secp256r1
    let (signing_key, public_key) = create_test_secp256r1_keypair();
    let (swig_key, _) = create_swig_secp256r1(&mut context, &public_key, id).unwrap();

    // The new authority to be added
    let (_, new_public_key) = create_test_secp256r1_keypair();

    // Get current slot and counter for the authority

    // Create the instruction to add the new authority.
    // This must be signed by the existing `public_key`.
    let instructions = swig_interface::AddAuthorityInstruction::new_with_secp256r1_authority(
        swig_key,
        context.default_payer.pubkey(),
        authority_fn,
        current_slot,
        next_counter,
        0, // role_id of the primary authority
        &public_key,
        AuthorityConfig {
            authority_type: AuthorityType::Secp256r1,
            authority: &new_public_key, // the new authority to be added
        },
        vec![ClientAction::All(All {})],
    )
    .unwrap();

    // To create instruction using swig_interface::CreateInstruction
    // To create message using v0::Message::try_compile
    // To send transaction using context.svm.send_transaction

    // Verify the authority was added
    let swig_account = context.svm.get_account(&swig_key).unwrap();
    let swig_state = SwigWithRoles::from_bytes(&swig_account.data).unwrap();
    assert_eq!(swig_state.state.roles, 2);

    // Verify the counter was incremented
    let new_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap();
    assert_eq!(
        new_counter, next_counter,
        "Counter should be incremented after successful transaction"
    );
}

C. Transaction Signing & Replay Protection:

Signing transactions with secp256r1 is more complex than with Ed25519. It involves a critical feature for security: the signature_odometer. The signature_odometer: This is an on-chain counter (u32) that is part of the Secp256r1Authority struct. A get_secp256r1_counter helper function reads this value from the swig account’s data.
/// Standard Secp256r1 authority implementation for passkey support.
///
/// This struct represents a Secp256r1 authority with a compressed public key
/// for signature verification using the Solana secp256r1 precompile program.
#[derive(Debug, no_padding::NoPadding)]
#[repr(C, align(8))]
pub struct Secp256r1Authority {
    /// The compressed Secp256r1 public key (33 bytes)
    pub public_key: [u8; 33],
    /// Padding for u32 alignment
    _padding: [u8; 3],
    /// Signature counter to prevent signature replay attacks
    pub signature_odometer: u32,
}
The test_secp256r1_basic_signing test provides a perfect, low-level example of the signing flow.
#[test_log::test]
fn test_secp256r1_basic_signing() {
    // Create a real secp256r1 key pair for testing

    // Create a new swig with the secp256r1 authority

    // Set up a recipient and transaction
    let transfer_ix = system_instruction::transfer(&swig_key, &recipient.pubkey(), transfer_amount);

    // Get the current on-chain counter
    let current_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap();
    let next_counter = current_counter + 1;

    // Create authority function that signs the message hash
    let mut authority_fn = |message_hash: &[u8]| -> [u8; 64] {
        use solana_secp256r1_program::sign_message;
        let signature =
            sign_message(message_hash, &signing_key.private_key_to_der().unwrap()).unwrap();
        signature
    };

    // Create the secp256r1 signing instructions (returns Vec<Instruction>)
    let instructions = swig_interface::SignInstruction::new_secp256r1(
        swig_key,
        context.default_payer.pubkey(),
        authority_fn,
        current_slot,
        next_counter,        // Provide the next expected counter
        transfer_ix.clone(), // The instruction to execute
        0,                   // The role ID of the secp256r1 key
        &public_key,
    )
    .unwrap();

    // Build and send the transaction
    let message = v0::Message::try_compile(
        &context.default_payer.pubkey(),
        &instructions, // These instructions include the secp256r1 program calls
        &[],
        context.svm.latest_blockhash(),
    )
    .unwrap();

    let tx =
        VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer])
            .unwrap();
    context.svm.send_transaction(tx).unwrap();

    // Verify the odometer was incremented on-chain
    let new_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap();
    assert_eq!(new_counter, next_counter);
}

D. Session Authorities:

SWIG also supports session-based secp256r1 authorities. These are temporary keys with a defined lifespan, measured in Solana slots.
/// Creation parameters for a session-based Secp256r1 authority.
#[derive(Debug, no_padding::NoPadding)]
#[repr(C, align(8))]
pub struct CreateSecp256r1SessionAuthority {
    /// The compressed Secp256r1 public key (33 bytes)
    pub public_key: [u8; 33],
    /// Padding for alignment
    _padding: [u8; 7],
    /// The session key for temporary authentication
    pub session_key: [u8; 32],
    /// Maximum duration a session can be valid for
    pub max_session_length: u64,
}

/// The `test_secp256r1_session_authority_odometer` test verifies that a session authority is
/// correctly initialized on-chain with its odometer set to 0.
#[test_log::test]
fn test_secp256r1_session_authority_odometer() {
    // setup

    // Create a swig with a session authority
    let (swig_key, _) =
        create_swig_secp256r1_session(&mut context, &public_key, id, 100, [0; 32]).unwrap();

    // get_session_counter helper to read the current odometer
    // Verify the initial counter is 0
    let initial_counter = get_session_counter(&context).unwrap();
    assert_eq!(initial_counter, 0, "Initial session counter should be 0");

    // Verify the authority type and session properties
    let swig_account = context.svm.get_account(&swig_key).unwrap();
    let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap();
    assert_eq!(swig.state.roles, 1);

    let role = swig.get_role(0).unwrap().unwrap();
    assert_eq!(
        role.authority.authority_type(),
        AuthorityType::Secp256r1Session
    );
    assert!(role.authority.session_based());

    let auth: &Secp256r1SessionAuthority = role.authority.as_any().downcast_ref().unwrap();
    assert_eq!(auth.max_session_age, 100);
    assert_eq!(auth.signature_odometer, 0, "Initial odometer should be 0");
}

Live Examples

Explore these working implementations to see secp256r1 authorities in action:

TypeScript secp256r1 Transfer

Complete TypeScript example demonstrating secp256r1 key generation, Swig creation, and transfer operations using LiteSVM

Passkey Helper Functions

TypeScript helper functions for WebAuthn passkey integration with secp256r1 authorities

Interactive Passkey UI

Complete UI example showing how to build user-facing passkey authentication with secp256r1

Key Differences: TypeScript vs Rust

AspectTypeScriptRust
Key GenerationUses @noble/curves/nist p256Uses OpenSSL EcKey with X9_62_PRIME256V1
Public Key FormatAuto-compressed via p256.getPublicKey(key, true)Manual compression via PointConversionForm::COMPRESSED
Signing FunctiongetSigningFnForSecp256r1PrivateKey()Custom closure with sign_message()
WebAuthn SupportBuilt-in passkey integrationRequires external WebAuthn library
Testing EnvironmentLiteSVM for lightweight testingSwigTestContext with full SVM
Use CasesClient-side apps, browsers, passkeysServer-side, CLI tools, testing
Both implementations provide the same cryptographic security and are fully compatible with the Swig protocol. Choose based on your deployment environment and integration needs.