Back to Blog
Technical

Building Secure APIs in 2024: A Cryptography-First Approach

Modern API security requires more than authentication tokens. Learn how to implement defense-in-depth with proper cryptographic primitives, from transport security to payload encryption.

Mamone TarshaMamone Tarsha
December 5, 2024
9 min read

Beyond Basic Authentication

Most API security guides focus on authentication—OAuth, API keys, JWTs. While important, authentication is just one layer. A truly secure API implements cryptographic protection at every level: transport, authentication, payload, and audit.

This guide covers the complete cryptographic stack for modern API security.

Layer 1: Transport Security

TLS Configuration

Every API must use TLS. But not all TLS configurations are equal:

# Modern TLS configuration
ssl_protocols TLSv1.3 TLSv1.2;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# HSTS with long duration
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Certificate transparency
ssl_ct on;
ssl_ct_static_scts /etc/nginx/scts;

Certificate Management

Automate certificate renewal to prevent outages:

# Certbot with automatic renewal
certbot certonly --webroot -w /var/www/html -d api.example.com
echo "0 0 * * * root certbot renew --quiet" >> /etc/crontab

Mutual TLS for Service-to-Service

For internal APIs, require client certificates:

ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;

Layer 2: Authentication

JWT Best Practices

JWTs are ubiquitous but often misconfigured:

use hpcrypt::signature::{Ed25519, Signer, Verifier};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Claims {
    sub: String,      // Subject (user ID)
    aud: String,      // Audience (API identifier)
    exp: u64,         // Expiration (Unix timestamp)
    iat: u64,         // Issued at
    jti: String,      // JWT ID (for revocation)
    scope: Vec<String>, // Permissions
}

fn create_jwt(claims: &Claims, signing_key: &Ed25519SigningKey) -> String {
    let header = base64url_encode(r#"{"alg":"EdDSA","typ":"JWT"}"#);
    let payload = base64url_encode(&serde_json::to_string(claims).unwrap());

    let message = format!("{}.{}", header, payload);
    let signature = signing_key.sign(message.as_bytes());

    format!("{}.{}", message, base64url_encode(&signature))
}

fn verify_jwt(token: &str, verifying_key: &Ed25519VerifyingKey) -> Result<Claims, Error> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return Err(Error::InvalidFormat);
    }

    let message = format!("{}.{}", parts[0], parts[1]);
    let signature = base64url_decode(parts[2])?;

    verifying_key.verify(message.as_bytes(), &signature)?;

    let claims: Claims = serde_json::from_slice(&base64url_decode(parts[1])?)?;

    // Validate expiration
    if claims.exp < current_unix_timestamp() {
        return Err(Error::Expired);
    }

    Ok(claims)
}

Key JWT security rules:

  1. Use asymmetric algorithms (EdDSA, ES256) not symmetric (HS256)
  2. Always validate exp, aud, and iss claims
  3. Keep tokens short-lived (15 minutes for access tokens)
  4. Implement token revocation for logout/compromise

API Key Security

For machine-to-machine authentication:

use hpcrypt::hash::Sha256;
use hpcrypt::kdf::Hkdf;

// Generate API key with embedded metadata
fn generate_api_key(client_id: &str) -> (String, [u8; 32]) {
    let mut rng = secure_rng();
    let secret: [u8; 32] = rng.gen();

    // Derive key ID from secret (for lookup without storing secret)
    let key_id = Sha256::hash(&secret)[..16].to_vec();
    let key_id_hex = hex::encode(&key_id);

    // Format: prefix_keyid_secret
    let api_key = format!("sk_live_{}_{}", key_id_hex, hex::encode(&secret));

    // Store only the hash of the secret
    let secret_hash = Sha256::hash(&secret);

    (api_key, secret_hash)
}

// Verify API key
fn verify_api_key(api_key: &str) -> Result<ClientId, Error> {
    let parts: Vec<&str> = api_key.split('_').collect();
    if parts.len() != 4 || parts[0] != "sk" {
        return Err(Error::InvalidFormat);
    }

    let key_id = parts[2];
    let secret = hex::decode(parts[3])?;
    let secret_hash = Sha256::hash(&secret);

    // Constant-time comparison against stored hash
    let stored_hash = db.get_key_hash(key_id)?;
    if !constant_time_eq(&secret_hash, &stored_hash) {
        return Err(Error::InvalidKey);
    }

    db.get_client_id(key_id)
}

Layer 3: Payload Security

Request Signing

Prevent tampering by signing request bodies:

use hpcrypt::mac::HmacSha256;

fn sign_request(
    method: &str,
    path: &str,
    body: &[u8],
    timestamp: u64,
    secret: &[u8; 32],
) -> String {
    let message = format!(
        "{}\n{}\n{}\n{}",
        method,
        path,
        timestamp,
        Sha256::hash(body).to_hex()
    );

    let signature = HmacSha256::mac(secret, message.as_bytes());
    base64::encode(&signature)
}

fn verify_request_signature(
    method: &str,
    path: &str,
    body: &[u8],
    timestamp: u64,
    signature: &str,
    secret: &[u8; 32],
) -> Result<(), Error> {
    // Reject old requests (prevent replay)
    let now = current_unix_timestamp();
    if timestamp < now - 300 || timestamp > now + 60 {
        return Err(Error::TimestampOutOfRange);
    }

    let expected = sign_request(method, path, body, timestamp, secret);
    if !constant_time_eq(signature.as_bytes(), expected.as_bytes()) {
        return Err(Error::InvalidSignature);
    }

    Ok(())
}

Field-Level Encryption

For sensitive fields, encrypt before transmission:

use hpcrypt::aead::{AesGcm256, Aead};

#[derive(Serialize, Deserialize)]
struct PaymentRequest {
    order_id: String,
    amount: u64,
    currency: String,

    #[serde(with = "encrypted_field")]
    card_number: String,

    #[serde(with = "encrypted_field")]
    cvv: String,
}

mod encrypted_field {
    use super::*;

    pub fn serialize<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
    where S: Serializer {
        let key = get_field_encryption_key();
        let nonce = generate_nonce();
        let ciphertext = AesGcm256::new(&key)
            .encrypt(&nonce, value.as_bytes(), &[])
            .map_err(serde::ser::Error::custom)?;

        let encoded = format!("{}:{}", hex::encode(&nonce), hex::encode(&ciphertext));
        serializer.serialize_str(&encoded)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
    where D: Deserializer<'de> {
        let encoded = String::deserialize(deserializer)?;
        let parts: Vec<&str> = encoded.split(':').collect();

        let nonce = hex::decode(parts[0]).map_err(serde::de::Error::custom)?;
        let ciphertext = hex::decode(parts[1]).map_err(serde::de::Error::custom)?;

        let key = get_field_encryption_key();
        let plaintext = AesGcm256::new(&key)
            .decrypt(&nonce, &ciphertext, &[])
            .map_err(serde::de::Error::custom)?;

        String::from_utf8(plaintext).map_err(serde::de::Error::custom)
    }
}

Layer 4: Audit and Compliance

Cryptographic Audit Logs

Create tamper-evident audit trails:

use hpcrypt::hash::Sha256;

#[derive(Serialize)]
struct AuditEntry {
    timestamp: u64,
    action: String,
    actor: String,
    resource: String,
    details: serde_json::Value,
    previous_hash: String,
    hash: String,
}

impl AuditEntry {
    fn new(
        action: &str,
        actor: &str,
        resource: &str,
        details: serde_json::Value,
        previous_hash: &str,
    ) -> Self {
        let mut entry = Self {
            timestamp: current_unix_timestamp(),
            action: action.to_string(),
            actor: actor.to_string(),
            resource: resource.to_string(),
            details,
            previous_hash: previous_hash.to_string(),
            hash: String::new(),
        };

        // Hash all fields except hash itself
        let content = serde_json::to_string(&entry).unwrap();
        entry.hash = Sha256::hash(content.as_bytes()).to_hex();

        entry
    }
}

fn verify_audit_chain(entries: &[AuditEntry]) -> bool {
    for i in 1..entries.len() {
        // Verify hash chain
        if entries[i].previous_hash != entries[i-1].hash {
            return false;
        }

        // Verify entry hash
        let mut entry = entries[i].clone();
        entry.hash = String::new();
        let expected_hash = Sha256::hash(
            serde_json::to_string(&entry).unwrap().as_bytes()
        ).to_hex();

        if entries[i].hash != expected_hash {
            return false;
        }
    }
    true
}

Security Checklist

Transport

  • TLS 1.2+ only, prefer TLS 1.3
  • Strong cipher suites (AEAD only)
  • HSTS enabled with long max-age
  • Certificate transparency
  • Automated certificate renewal

Authentication

  • Asymmetric JWT algorithms
  • Short token lifetimes
  • Token revocation mechanism
  • API keys hashed in storage
  • Rate limiting per client

Payload

  • Request signing for mutations
  • Field-level encryption for PII
  • Input validation before decryption
  • Size limits on encrypted fields

Audit

  • Hash-chained audit logs
  • Immutable log storage
  • Regular chain verification
  • Anomaly detection

Conclusion

API security is a layered discipline. Each layer provides defense against different attack vectors:

  • Transport: Protects data in transit
  • Authentication: Verifies identity
  • Payload: Ensures integrity and confidentiality
  • Audit: Enables detection and forensics

Implementing all four layers creates defense-in-depth that can withstand sophisticated attacks. Start with the basics (TLS, authentication) and progressively add payload security and audit capabilities as your API matures.

Interested in learning more?

Get in touch with our team to discuss how we can help with your cryptography needs.

Book a Meeting