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 ⚠️

  1. One-Time Keys Only: NEVER reuse a Poly1305 key - catastrophic security failure
  2. Not Encryption: Poly1305 provides authentication only, not confidentiality
  3. Key Generation: First 16 bytes (r) need clamping, last 16 bytes (s) are mask
  4. Tag Truncation: NEVER truncate the 16-byte tag
  5. Standalone Dangerous: Use with cipher (ChaCha20-Poly1305) for complete security

API Functions

poly1305_auth(message: bytes, key: bytes[32]) -> bytes[16]

Security Contract:

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:

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:

Security Notes:

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:

Security Requirements:

poly1305_final(ctx: Poly1305Context) -> bytes[16]

Security Contract:

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

Security Auditing

Verification Checklist

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:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. Poly1305 Paper
  2. RFC 8439 - ChaCha20-Poly1305
  3. Poly1305-AES Paper

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