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:
- Use asymmetric algorithms (EdDSA, ES256) not symmetric (HS256)
- Always validate
exp,aud, andissclaims - Keep tokens short-lived (15 minutes for access tokens)
- 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.