More than authentication
Most API security guides focus on authentication (OAuth, API keys, JWTs), which matters but is only one layer. A secure API applies cryptographic protection at every level: transport (protecting data in transit), authentication (verifying identity), payload (integrity and confidentiality) and audit (detection and forensics).
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 between services
For internal APIs, require client certificates:
ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
Layer 2: authentication
Signing and verifying JWTs
JWTs are everywhere and often misconfigured:
use opencrypt::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)
}
JWT rules:
- Use asymmetric algorithms (EdDSA, ES256), not symmetric ones (HS256)
- Always validate the
exp,audandissclaims - Keep tokens short-lived (15 minutes for access tokens)
- Implement token revocation for logout and compromise
API keys
For machine-to-machine authentication:
use opencrypt::hash::Sha256;
use opencrypt::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
Sign request bodies to prevent tampering:
use opencrypt::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
Encrypt sensitive fields before transmission:
use opencrypt::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
Make the audit trail tamper-evident:
use opencrypt::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
Each layer covers different attacks, and together the four give defense in depth that can withstand sophisticated attacks. Start with TLS and authentication, and add payload security and audit logging as the API matures.