Message Authentication

Message Authentication

Message Authentication Codes (MACs) provide data authenticity and integrity using a shared secret key. They verify that a message came from a legitimate sender and hasn’t been tampered with.

What is a MAC?

A MAC is like a “keyed checksum” - it uses a secret key to produce an authentication tag that only someone with the key can generate or verify. Unlike digital signatures, MACs use symmetric keys (same key for generation and verification).

Available MAC Algorithms

HMAC ⭐ Recommended

Hash-based Message Authentication Code

  • Uses any hash function (HMAC-SHA256, HMAC-SHA512, HMAC-BLAKE3)
  • RFC 2104 standardised
  • Fast and secure
  • Most widely used MAC

Use HMAC for:

  • API request authentication
  • File integrity verification
  • Message authentication
  • General-purpose MAC needs

CMAC

Cipher-based Message Authentication Code

  • Uses block ciphers (AES-CMAC)
  • NIST SP 800-38B standardised
  • Good alternative to HMAC

Poly1305

Universal hash function MAC

  • Very fast (designed for speed)
  • Used in ChaCha20-Poly1305
  • 128-bit security (one-time key)

Quick Comparison

AlgorithmSpeedSecurityUse CaseRecommended
HMAC-SHA256Fast256-bitGeneral purpose⭐ Primary
HMAC-BLAKE3Very Fast256-bitHigh throughput⭐ Performance
HMAC-SHA512Fast512-bitHigh security⭐ Long-term
CMAC-AESMedium128-bitBlock cipher preferenceAlternative
Poly1305Very Fast128-bitWith ChaCha20Specialised
GMACVery Fast128-bitWith AES-GCMSpecialised

Common Interface

All MAC algorithms implement the MessageAuthenticationCode interface:

class MessageAuthenticationCode {
public:
    // Set authentication key
    void SetKey(const byte* key, size_t length);

    // Add data to authenticate
    void Update(const byte* input, size_t length);

    // Get MAC tag
    void Final(byte* mac);

    // Reset for new message (keeps key)
    void Restart();

    // Query methods
    unsigned int DigestSize() const;  // MAC tag size
    unsigned int OptimalBlockSize() const;
};

Choosing a MAC Algorithm

For New Systems

Primary Choice: HMAC-SHA256

HMAC<SHA256> hmac(key, keyLen);
  • Industry standard
  • Hardware acceleration on modern CPUs
  • Well-studied and trusted
  • Good balance of speed and security

Performance-Critical: HMAC-BLAKE3

HMAC<BLAKE3> hmac(key, keyLen);
  • 2-4x faster than HMAC-SHA256
  • Same 256-bit security
  • Excellent for high-throughput systems

High Security: HMAC-SHA512

HMAC<SHA512> hmac(key, keyLen);
  • 512-bit security
  • Future-proof
  • Faster on 64-bit systems

For Specific Scenarios

With AES Encryption: Use AES-GCM (provides encryption + MAC together)

With ChaCha20 Encryption: Use ChaCha20-Poly1305 (provides encryption + MAC together)

Block Cipher Preference: Use AES-CMAC

MAC vs Digital Signature

FeatureMACDigital Signature
Key typeSymmetric (shared)Asymmetric (public/private)
VerificationSame keyPublic key
Non-repudiation❌ No✅ Yes
Speed⚡⚡⚡ Very fast⚡ Slower
Use caseTrusted partiesUntrusted parties

When to use MAC: Both parties trust each other (API authentication, file integrity)

When to use signatures: Need non-repudiation (software signing, contracts)

Security Best Practices

1. Key Generation

AutoSeededRandomPool rng;
SecByteBlock key(32);  // 256-bit key for HMAC
rng.GenerateBlock(key, key.size());

2. Key Length

  • Minimum: 128 bits (16 bytes)
  • Recommended: 256 bits (32 bytes)
  • Never: < 128 bits

3. Constant-Time Verification

// WRONG - vulnerable to timing attacks
if (computedMAC == receivedMAC) {
    // Attacker can measure comparison time
}

// CORRECT - constant-time comparison
bool valid = VerifyMAC(computedMAC, receivedMAC);

4. Don’t Use for Passwords

// WRONG - MACs are not password hashing functions
std::string hash = hmac(password);

// CORRECT - use password hashing
Argon2 argon2;
argon2.DeriveKey(hash, ...);

5. Key Storage

SecByteBlock key(32);  // Auto-zeroes on destruction

// NOT: std::string key;  // Leaves key in memory
// NOT: byte key[32];     // Not auto-zeroed

Common Patterns

API Request Signing

std::string signRequest(const std::string& method,
                       const std::string& path,
                       const std::string& body,
                       const SecByteBlock& apiKey) {
    HMAC<SHA256> hmac(apiKey, apiKey.size());

    std::string message = method + "\n" + path + "\n" + body;
    std::string mac, signature;

    StringSource(message, true,
        new HashFilter(hmac, new StringSink(mac))
    );

    // Encode as hex or base64
    StringSource(mac, true,
        new HexEncoder(new StringSink(signature))
    );

    return signature;
}

File Integrity

std::string computeFileMAC(const std::string& filename,
                           const SecByteBlock& key) {
    HMAC<SHA256> hmac(key, key.size());
    std::string mac, hexMAC;

    FileSource(filename.c_str(), true,
        new HashFilter(hmac, new StringSink(mac))
    );

    StringSource(mac, true,
        new HexEncoder(new StringSink(hexMAC))
    );

    return hexMAC;
}

Encrypt-then-MAC

void authenticatedEncryption(const std::string& plaintext,
                             const SecByteBlock& encKey,
                             const SecByteBlock& macKey,
                             std::string& ciphertext,
                             std::string& mac) {
    // 1. Encrypt
    // ... encrypt plaintext to ciphertext ...

    // 2. MAC the ciphertext (Encrypt-then-MAC)
    HMAC<SHA256> hmac(macKey, macKey.size());
    StringSource(ciphertext, true,
        new HashFilter(hmac, new StringSink(mac))
    );
}

// Note: In practice, use AES-GCM which combines encryption + MAC

Performance Benchmarks

Approximate speeds on modern hardware:

AlgorithmSpeed (MB/s)Hardware Accel
HMAC-BLAKE33000-6000No
HMAC-SHA256800-1500SHA-NI
HMAC-SHA512600-1200SHA-NI
Poly13052000-4000No
CMAC-AES1000-2000AES-NI

Thread Safety

MAC objects are not thread-safe. Create separate instances per thread:

// WRONG - sharing between threads
HMAC<SHA256> shared_hmac;

// CORRECT - thread-local instances
void threadFunc(const SecByteBlock& key) {
    HMAC<SHA256> hmac(key, key.size());  // Per-thread
    // ... use hmac ...
}

When NOT to Use MACs

❌ Password Hashing

MACs are fast - that’s bad for passwords. Use Argon2:

// WRONG
HMAC<SHA256> hmac(salt, saltLen);
std::string hash = hmac(password);

// CORRECT
Argon2 argon2;
argon2.DeriveKey(hash, ...);

❌ Digital Signatures

MACs require shared secrets. For public verification, use signatures:

// WRONG - both parties need same secret
HMAC<SHA256> hmac(sharedSecret, secretLen);

// CORRECT - public key verification
Ed25519::Signer signer(privateKey);
Ed25519::Verifier verifier(publicKey);

❌ Content Addressing

For identifying content without authentication, use hashes:

// If you don't need authentication, just use SHA-256
SHA256 hash;
std::string digest = hash(data);

Authenticated Encryption (AEAD)

Instead of separate encryption + MAC, use authenticated encryption:

Instead of…Use…
AES-CBC + HMAC-SHA256AES-GCM
AES-CTR + HMAC-SHA256AES-GCM
ChaCha20 + HMACChaCha20-Poly1305

Benefits:

  • Simpler API (one operation)
  • Faster (optimised together)
  • Harder to misuse (correct by construction)

See Also