Poly1305 Security-Focused API Documentation
Version: 1.0
Last Updated: 2025-07-05
**Security Classification: PUBLIC
Author: Phantom (phantom@metamui.id)
Overview
Poly1305 is a high-speed message authentication code (MAC) designed by Daniel J. Bernstein. It provides 128-bit authentication tags using a 256-bit one-time key. Poly1305 is commonly used with ChaCha20 in the ChaCha20-Poly1305 AEAD construction but can be used with any secure cipher.
Security Level: 128-bit authentication
Key Size: 32 bytes (256 bits) - one-time use only
Tag Size: 16 bytes (128 bits)
Block Size: 16 bytes
Security Warnings ⚠️
- One-Time Keys Only: NEVER reuse a Poly1305 key - catastrophic security failure
- Not Encryption: Poly1305 provides authentication only, not confidentiality
- Key Generation: First 16 bytes (r) need clamping, last 16 bytes (s) are mask
- Tag Truncation: NEVER truncate the 16-byte tag
- Standalone Dangerous: Use with cipher (ChaCha20-Poly1305) for complete security
API Functions
poly1305_auth(message: bytes, key: bytes[32]) -> bytes[16]
Security Contract:
- Preconditions:
keyMUST be 32 bytes of fresh random datakeyMUST be used only oncemessagecan be any length
- Postconditions:
- Returns 16-byte authentication tag
- Tag uniquely identifies message under key
- Different messages produce different tags
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Forgery | ✅ | 2^128 attempts needed | | Key Recovery | ✅ | From single tag | | Collision | ✅ | Different messages = different tags | | Timing Attack | ✅ | Constant-time implementation | | Tag Truncation | ❌ | Never truncate | | Key Reuse | ❌ | Complete break if key reused |
Security Requirements:
- Generate fresh key for every message
- Use secure RNG for key generation
- Never store or reuse keys
- Always verify full 16-byte tag
Secure Usage Example:
import poly1305
import chacha20
import secrets
from typing import Tuple
class SecurePoly1305:
"""Secure Poly1305 usage with ChaCha20"""
@staticmethod
def generate_poly1305_key(
chacha_key: bytes,
nonce: bytes
) -> bytes:
"""Generate one-time Poly1305 key using ChaCha20"""
# Generate 32 bytes using ChaCha20
# First 32 bytes of keystream
key_generator = chacha20.ChaCha20(chacha_key, nonce)
poly_key = key_generator.encrypt(b'\x00' * 32)
# Clamp r (first 16 bytes) as required
r = bytearray(poly_key[:16])
# Clamp r: clear required bits
r[3] &= 15
r[7] &= 15
r[11] &= 15
r[15] &= 15
r[4] &= 252
r[8] &= 252
r[12] &= 252
# Return clamped r + s
return bytes(r) + poly_key[16:32]
@staticmethod
def authenticate_message(
message: bytes,
key: bytes,
additional_data: bytes = b""
) -> bytes:
"""Authenticate message with Poly1305"""
if len(key) != 32:
raise ValueError("Key must be exactly 32 bytes")
# Construct input with AD if present
if additional_data:
# Include AD length for domain separation
auth_input = (
len(additional_data).to_bytes(8, 'little') +
additional_data +
len(message).to_bytes(8, 'little') +
message
)
else:
auth_input = message
# Compute tag
tag = poly1305.authenticate(auth_input, key)
# Immediately clear the key
secure_zero(key)
return tag
@staticmethod
def verify_tag(
message: bytes,
tag: bytes,
key: bytes,
additional_data: bytes = b""
) -> bool:
"""Verify Poly1305 tag with constant-time comparison"""
if len(tag) != 16:
return False
# Recompute tag
expected_tag = SecurePoly1305.authenticate_message(
message, key, additional_data
)
# Constant-time comparison
return hmac.compare_digest(expected_tag, tag)
# SECURE: ChaCha20-Poly1305 AEAD implementation
class ChaCha20Poly1305:
"""AEAD construction using ChaCha20 and Poly1305"""
def __init__(self, key: bytes):
if len(key) != 32:
raise ValueError("Key must be 32 bytes")
self.key = key
def encrypt(
self,
plaintext: bytes,
nonce: bytes,
associated_data: bytes = b""
) -> Tuple[bytes, bytes]:
"""Encrypt and authenticate with ChaCha20-Poly1305"""
if len(nonce) != 12:
raise ValueError("Nonce must be 12 bytes")
# Generate Poly1305 key
poly_key = SecurePoly1305.generate_poly1305_key(self.key, nonce)
# Encrypt plaintext
cipher = chacha20.ChaCha20(self.key, nonce, counter=1)
ciphertext = cipher.encrypt(plaintext)
# Construct MAC input per RFC 8439
mac_data = self._build_mac_data(
associated_data,
ciphertext
)
# Authenticate
tag = poly1305.authenticate(mac_data, poly_key)
# Clear Poly1305 key
secure_zero(poly_key)
return ciphertext, tag
def _build_mac_data(
self,
aad: bytes,
ciphertext: bytes
) -> bytes:
"""Build input for Poly1305 per RFC 8439"""
# AAD || pad16(AAD) || ciphertext || pad16(ciphertext) ||
# len(AAD) || len(ciphertext)
mac_data = bytearray()
# AAD with padding
mac_data.extend(aad)
if len(aad) % 16 != 0:
mac_data.extend(b'\x00' * (16 - len(aad) % 16))
# Ciphertext with padding
mac_data.extend(ciphertext)
if len(ciphertext) % 16 != 0:
mac_data.extend(b'\x00' * (16 - len(ciphertext) % 16))
# Lengths (little-endian 64-bit)
mac_data.extend(len(aad).to_bytes(8, 'little'))
mac_data.extend(len(ciphertext).to_bytes(8, 'little'))
return bytes(mac_data)
# SECURE: Poly1305 with nonce to prevent reuse
class NoncedPoly1305:
"""Poly1305 with nonce mixing to prevent key reuse"""
def __init__(self, master_key: bytes):
if len(master_key) != 32:
raise ValueError("Master key must be 32 bytes")
self.master_key = master_key
def create_tag(self, message: bytes, nonce: bytes) -> bytes:
"""Create tag with nonce-derived key"""
# Derive unique key from master + nonce
h = blake2b.new(digest_size=32, key=self.master_key)
h.update(b"Poly1305-Nonce-v1")
h.update(nonce)
derived_key = h.digest()
# Compute tag
tag = poly1305.authenticate(message, derived_key)
# Clear derived key
secure_zero(derived_key)
return tag
Common Mistakes:
# CATASTROPHIC: Key reuse
key = secrets.token_bytes(32)
tag1 = poly1305.authenticate(message1, key)
tag2 = poly1305.authenticate(message2, key) # BROKEN!
# INSECURE: Short/weak key
key = b"password123".ljust(32, b'\x00') # NOT RANDOM!
# INSECURE: Tag truncation
tag = poly1305.authenticate(message, key)
short_tag = tag[:8] # NEVER DO THIS!
# INCORRECT: Not clamping r
key = secrets.token_bytes(32) # Need to clamp first 16 bytes!
poly1305_init(key: bytes[32]) -> Poly1305Context
Security Contract:
- Preconditions:
keymust be properly formatted (r clamped)- Fresh key for each context
- Postconditions:
- Returns context for incremental authentication
- Context contains key material
- Ready for update calls
Security Notes:
- Context contains secret key material
- Never serialize/store context
- Use once and discard
Secure Usage Example:
use poly1305::{Poly1305, Key, Tag};
use poly1305::universal_hash::{UniversalHash, NewUniversalHash};
use chacha20::{ChaCha20, cipher::{NewCipher, StreamCipher}};
/// Secure streaming Poly1305
pub struct StreamingAuthenticator {
poly: Poly1305,
processed: usize,
}
impl StreamingAuthenticator {
pub fn new(key: &[u8; 32]) -> Result<Self, Error> {
// Verify key is clamped
let r = &key[..16];
if !Self::is_clamped(r) {
return Err(Error::InvalidKey);
}
let key = Key::from_slice(key);
let poly = Poly1305::new(key);
Ok(Self {
poly,
processed: 0,
})
}
pub fn update(&mut self, data: &[u8]) {
self.poly.update_padded(data);
self.processed += data.len();
}
pub fn finalize(self) -> Tag {
self.poly.finalize()
}
fn is_clamped(r: &[u8]) -> bool {
r[3] & 0xF0 == 0 &&
r[7] & 0xF0 == 0 &&
r[11] & 0xF0 == 0 &&
r[15] & 0xF0 == 0 &&
r[4] & 0x03 == 0 &&
r[8] & 0x03 == 0 &&
r[12] & 0x03 == 0
}
}
/// Poly1305-AES variant
pub struct Poly1305AES {
aes_key: [u8; 16],
poly_r: [u8; 16],
}
impl Poly1305AES {
pub fn new(key: &[u8; 32]) -> Self {
let mut aes_key = [0u8; 16];
let mut poly_r = [0u8; 16];
aes_key.copy_from_slice(&key[..16]);
poly_r.copy_from_slice(&key[16..]);
// Clamp r
poly_r[3] &= 15;
poly_r[7] &= 15;
poly_r[11] &= 15;
poly_r[15] &= 15;
poly_r[4] &= 252;
poly_r[8] &= 252;
poly_r[12] &= 252;
Self { aes_key, poly_r }
}
pub fn authenticate(&self, message: &[u8], nonce: &[u8; 16]) -> [u8; 16] {
// Generate s = AES(k, n)
let s = aes_encrypt_block(&self.aes_key, nonce);
// Build full Poly1305 key
let mut poly_key = [0u8; 32];
poly_key[..16].copy_from_slice(&self.poly_r);
poly_key[16..].copy_from_slice(&s);
// Authenticate
let key = Key::from_slice(&poly_key);
let poly = Poly1305::new(key);
poly.compute_unpadded(message).into()
}
}
poly1305_update(ctx: &mut Poly1305Context, data: bytes) -> void
Security Contract:
- Preconditions:
- Context initialized and not finalized
- Can be called multiple times
- Postconditions:
- Internal state updated
- Order matters for authentication
- Cannot undo updates
Security Requirements:
- Process data in order
- Include all data in tag
- No length limits
poly1305_final(ctx: Poly1305Context) -> bytes[16]
Security Contract:
- Preconditions:
- Context has been initialized
- Called exactly once per context
- Postconditions:
- Returns 16-byte authentication tag
- Context is consumed
- Tag authenticates all updated data
Security Best Practices
Key Management
class Poly1305KeyManager:
"""Secure key generation for Poly1305"""
@staticmethod
def generate_clamped_key() -> bytes:
"""Generate properly clamped Poly1305 key"""
# Generate 32 random bytes
key = bytearray(secrets.token_bytes(32))
# Clamp r (first 16 bytes)
key[3] &= 15
key[7] &= 15
key[11] &= 15
key[15] &= 15
key[4] &= 252
key[8] &= 252
key[12] &= 252
return bytes(key)
@staticmethod
def derive_poly_key(master_key: bytes, context: bytes) -> bytes:
"""Derive Poly1305 key from master key"""
# Use HKDF to derive
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"Poly1305-Derive-v1",
info=context
)
derived = hkdf.derive(master_key)
# Clamp the r part
derived_array = bytearray(derived)
derived_array[3] &= 15
derived_array[7] &= 15
derived_array[11] &= 15
derived_array[15] &= 15
derived_array[4] &= 252
derived_array[8] &= 252
derived_array[12] &= 252
return bytes(derived_array)
Secure Protocol Integration
/// Secure message protocol with Poly1305
pub struct SecureMessageProtocol {
shared_secret: [u8; 32],
send_counter: u64,
recv_counter: u64,
}
impl SecureMessageProtocol {
pub fn send_message(&mut self, message: &[u8]) -> Vec<u8> {
// Generate unique key for this message
let poly_key = self.derive_message_key(self.send_counter);
// Create header
let header = MessageHeader {
version: 1,
sequence: self.send_counter,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
};
// Serialize header
let header_bytes = bincode::serialize(&header).unwrap();
// Authenticate header + message
let mut mac_input = Vec::new();
mac_input.extend_from_slice(&header_bytes);
mac_input.extend_from_slice(&(message.len() as u64).to_le_bytes());
mac_input.extend_from_slice(message);
let tag = poly1305::authenticate(&mac_input, &poly_key);
// Clear key immediately
poly_key.zeroize();
self.send_counter += 1;
// Return header || tag || message
let mut output = header_bytes;
output.extend_from_slice(&tag);
output.extend_from_slice(message);
output
}
fn derive_message_key(&self, counter: u64) -> [u8; 32] {
use blake2::{Blake2b512, Digest};
let mut hasher = Blake2b512::new();
hasher.update(b"MessageKey-v1");
hasher.update(&self.shared_secret);
hasher.update(&counter.to_le_bytes());
let hash = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&hash[..32]);
// Clamp r
key[3] &= 15;
key[7] &= 15;
key[11] &= 15;
key[15] &= 15;
key[4] &= 252;
key[8] &= 252;
key[12] &= 252;
key
}
}
Common Integration Patterns
Authenticated Encryption
def authenticated_encrypt(
key: bytes,
nonce: bytes,
plaintext: bytes,
aad: bytes = b""
) -> dict:
"""AEAD using ChaCha20-Poly1305"""
# Derive Poly1305 key
cipher = ChaCha20(key, nonce)
poly_key = cipher.encrypt(b'\x00' * 32)
# Clamp r
r = bytearray(poly_key[:16])
r[3] &= 15
r[7] &= 15
r[11] &= 15
r[15] &= 15
r[4] &= 252
r[8] &= 252
r[12] &= 252
poly_key = bytes(r) + poly_key[16:32]
# Encrypt (counter starts at 1)
cipher = ChaCha20(key, nonce, counter=1)
ciphertext = cipher.encrypt(plaintext)
# Build MAC data
mac_data = bytearray()
mac_data.extend(aad)
mac_data.extend(b'\x00' * (16 - len(aad) % 16) if len(aad) % 16 else b'')
mac_data.extend(ciphertext)
mac_data.extend(b'\x00' * (16 - len(ciphertext) % 16) if len(ciphertext) % 16 else b'')
mac_data.extend(len(aad).to_bytes(8, 'little'))
mac_data.extend(len(ciphertext).to_bytes(8, 'little'))
# Compute tag
tag = poly1305.authenticate(bytes(mac_data), poly_key)
# Clear Poly1305 key
secure_zero(poly_key)
return {
'nonce': nonce,
'ciphertext': ciphertext,
'tag': tag,
'algorithm': 'ChaCha20-Poly1305'
}
High-Speed Authentication
/// Poly1305 for high-speed packet authentication
pub struct PacketAuthenticator {
master_key: [u8; 32],
}
impl PacketAuthenticator {
pub fn authenticate_packet(&self, packet: &Packet) -> [u8; 16] {
// Derive per-packet key
let mut hasher = Blake2b512::new();
hasher.update(&self.master_key);
hasher.update(&packet.id.to_le_bytes());
let hash = hasher.finalize();
let mut poly_key = [0u8; 32];
poly_key.copy_from_slice(&hash[..32]);
// Clamp r
poly_key[3] &= 15;
poly_key[7] &= 15;
poly_key[11] &= 15;
poly_key[15] &= 15;
poly_key[4] &= 252;
poly_key[8] &= 252;
poly_key[12] &= 252;
// Authenticate
let key = Key::from_slice(&poly_key);
let mut poly = Poly1305::new(key);
// Add packet fields
poly.update_padded(&packet.src_addr.octets());
poly.update_padded(&packet.dst_addr.octets());
poly.update_padded(&packet.protocol.to_le_bytes());
poly.update_padded(&packet.length.to_le_bytes());
poly.update_padded(&packet.data);
poly.finalize().into()
}
}
Performance Considerations
| Platform | Speed | vs HMAC-SHA256 | Notes |
|---|---|---|---|
| x86-64 | 1.5 cycles/byte | 3x faster | With AVX2 |
| ARM64 | 2.0 cycles/byte | 2.5x faster | NEON optimized |
| 32-bit | 3.5 cycles/byte | 2x faster | Still efficient |
| Small msg | ~150 cycles | 2x faster | Low overhead |
Optimization Strategies
- Pre-compute clamped r values
- Use SIMD implementations
- Batch verification when possible
- Cache derived keys appropriately
Security Auditing
Verification Checklist
- Keys used exactly once
- R component properly clamped
- Full 16-byte tags verified
- No tag truncation
- Keys cleared after use
- Proper nonce handling in AEAD
Common Vulnerabilities
# AUDIT: Critical patterns to detect
# ❌ Key reuse
key = get_static_key() # CATASTROPHIC!
for msg in messages:
tag = poly1305(msg, key)
# ❌ Missing r clamping
key = random_bytes(32) # Not clamped!
tag = poly1305(msg, key)
# ❌ Tag truncation
if tag[:8] == expected_tag[:8]: # NO!
accept_message()
# ❌ Weak key generation
key = hash(password).ljust(32, b'\x00') # NOT RANDOM!
Security Analysis
Threat Model: Poly1305 Threat Model
The comprehensive threat analysis covers:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
Support
Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md
Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC