Deep dive
How it works
Parts:
- K — long-term key, only in the card (SIM or secure element) and the home auth centre (HE/AuC).
- OPc — operator constant mixed into the f-functions (Milenage: OPc = OP xor E_K(OP)).
- f1, f1* — MACs; f2 gives RES; f3, f4 give CK, IK; f5, f5* give the anonymity key AK.
- SQN — 48-bit counter: home keeps SQN_HE; the card keeps SQN_MS (highest accepted) and an array of recent entries.
- Vector — RAND, XRES, CK, IK, AUTN from home, used once by the serving network; AUTN = (SQN xor AK) ‖ AMF ‖ MAC, MAC = f1(SQN ‖ RAND ‖ AMF).
Steps (TS 33.102 clause 6.3):
- Home draws RAND (CSPRNG) and the next SQN, computes MAC, XRES, CK, IK, AK, builds AUTN, and sends a batch of vectors to the serving network.
- Serving network sends RAND ‖ AUTN of one unused vector to the card.
- Card computes AK = f5(RAND), unmasks SQN, computes XMAC = f1(SQN ‖ RAND ‖ AMF). XMAC differs from MAC: network not genuine, abandon.
- Card checks SQN against its accepted range. Stale: sync failure with AUTS = (SQN_MS xor f5*(RAND)) ‖ f1*(SQN_MS ‖ RAND ‖ 0).
- Otherwise card returns RES = f2(RAND) and derives CK and IK.
- Serving network compares RES with XRES; equal means the card holds K. CK and IK then protect the link.
- After a sync failure the serving network sends RAND and AUTS to home; home recovers SQN_MS, verifies MAC-S, resets its counter, sends fresh vectors (clause 6.3.5).
Card (K, OPc, SQN_MS) Serving network Home (K, OPc, SQN_HE)
|-- auth data request ---->|
|<-- AV(1..n) -------------| RAND,XRES,CK,IK,AUTN
|<-- RAND ‖ AUTN ---------------|
| f5 -> AK -> SQN; f1 -> XMAC == MAC ?; SQN fresh ?
|-- RES ------------------------> RES == XRES ?
|-- AUTS (if stale) ----------->|-- RAND, AUTS ----------->| reset SQN_HE
Freshness. Annex C (informative) splits SQN = SEQ ‖ IND. The card keeps SEQ_MS(i) for each index i of an array. It accepts SEQ if SEQ > SEQ_MS(IND) and SEQ is at most Δ above its highest; otherwise it reports its highest SQN. Different indices tolerate out-of-order use.
Algorithm sets. Milenage (TS 35.206) is built on AES-128. TUAK (TS 35.231) uses the Keccak-f[1600] permutation, with 128 or 256-bit K and RES of 32 to 256 bits.
Newer generations. In 5G AKA (TS 33.501 clause 6.1.3.2) the home returns XRES*; the AUSF keeps it and gives the serving network only HXRES*, the 128 least significant bits of SHA-256(RAND ‖ XRES*). The serving network checks HRES* against HXRES*, then forwards RES* so the home network confirms the result. EAP-AKA' (RFC 9048, which updates RFC 4187 and RFC 5448) runs AKA inside EAP with keys bound to the access network name. 5G sends the identity as a concealed SUCI (ECIES profile A with X25519, or B with secp256r1).
Code
Rust, std only. The flow is real; the f-functions are a toy keyed mix (not Milenage, not secure) behind a function type, so Milenage or TUAK can be plugged in. The card uses an Annex C style array (size 4).
use std::{hint::black_box, time::Instant};
// (K, OPc, which f, inputs) -> 64 bits; plug in Milenage or TUAK here
type F = fn(&[u64; 2], u64, u8, [u64; 3]) -> u64;
const F1: u8 = 1; const F2: u8 = 2; const F5: u8 = 5; const F1S: u8 = 6; const F5S: u8 = 7; // f3, f4 (CK, IK) work like f2
const M48: u64 = (1 << 48) - 1;
// toy keyed mix: NOT Milenage, NOT secure
fn toy(k: &[u64; 2], opc: u64, w: u8, d: [u64; 3]) -> u64 {
let mut h = k[0] ^ (w as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
for x in [opc, d[0], d[1], d[2], k[1]] {
h = (h ^ x).wrapping_mul(0xff51_afd7_ed55_8ccd);
h ^= h >> 29;
}
h
}
type Autn = (u64, u16, u64); // (SQN xor AK, AMF, MAC)
struct Av { rand: u64, xres: u64, autn: Autn } // CK, IK omitted
struct Home { k: [u64; 2], opc: u64, seq: u64, ind: u64, f: F }
impl Home {
fn vector(&mut self, rand: u64) -> Av {
self.seq += 1;
self.ind = (self.ind + 1) % 4; // SQN = SEQ || IND, array size 4
let (sqn, amf) = (self.seq << 2 | self.ind, 0x8000u16);
let g = |w, d| (self.f)(&self.k, self.opc, w, d);
let ak = g(F5, [rand, 0, 0]) & M48;
Av { rand, xres: g(F2, [rand, 0, 0]), autn: (sqn ^ ak, amf, g(F1, [sqn, rand, amf as u64])) }
}
fn resync(&mut self, rand: u64, (conc, mac_s): (u64, u64)) -> bool {
let g = |w, d| (self.f)(&self.k, self.opc, w, d);
let sqn_ms = conc ^ (g(F5S, [rand, 0, 0]) & M48);
let ok = g(F1S, [sqn_ms, rand, 0]) == mac_s; // AMF = 0 in MAC-S
if ok { self.seq = sqn_ms >> 2; }
ok
}
}
#[derive(Clone, Copy)]
struct Card { k: [u64; 2], opc: u64, arr: [u64; 4], top: u64, f: F }
#[derive(Debug, PartialEq)]
enum Out { Ok(u64), MacFail, Sync((u64, u64)) } // RES, or AUTS = (Conc(SQN_ms), MAC-S)
impl Card {
fn challenge(&mut self, rand: u64, a: Autn) -> Out {
let g = |w, d| (self.f)(&self.k, self.opc, w, d);
let sqn = (a.0 ^ g(F5, [rand, 0, 0])) & M48; // undo the anonymity key
if g(F1, [sqn, rand, a.1 as u64]) != a.2 { return Out::MacFail; } // network not authentic
let (seq, ind) = (sqn >> 2, (sqn & 3) as usize);
if seq <= self.arr[ind] { // stale: report highest accepted SQN
return Out::Sync((self.top ^ (g(F5S, [rand, 0, 0]) & M48), g(F1S, [self.top, rand, 0])));
}
self.arr[ind] = seq;
if seq > self.top >> 2 { self.top = sqn; }
Out::Ok(g(F2, [rand, 0, 0]))
}
}
fn main() {
let (k, opc) = ([0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210], 0xa5a5_1234);
let mut home = Home { k, opc, seq: 0, ind: 0, f: toy };
let mut card = Card { k, opc, arr: [0; 4], top: 0, f: toy };
let vs: Vec<Av> = (0..4).map(|i| home.vector(1000 + i)).collect();
// happy path, out of order (v2 then v1)
assert_eq!(card.challenge(vs[2].rand, vs[2].autn), Out::Ok(vs[2].xres));
assert_eq!(card.challenge(vs[1].rand, vs[1].autn), Out::Ok(vs[1].xres));
// replay: SQN check fails
assert!(matches!(card.challenge(vs[2].rand, vs[2].autn), Out::Sync(_)));
// tampered AUTN, wrong K: MAC check fails
let mut bad = vs[3].autn; bad.2 ^= 1;
assert_eq!(card.challenge(vs[3].rand, bad), Out::MacFail);
bad = vs[3].autn; bad.0 ^= 4;
assert_eq!(card.challenge(vs[3].rand, bad), Out::MacFail);
let mut other = Card { k: [1, 2], ..card };
assert_eq!(other.challenge(vs[3].rand, vs[3].autn), Out::MacFail);
// home restored from backup: resync, then success
for i in 0..8 { let v = home.vector(2000 + i); card.challenge(v.rand, v.autn); }
let mut old = Home { k, opc, seq: 0, ind: 0, f: toy };
let v = old.vector(3000);
let Out::Sync(auts) = card.challenge(v.rand, v.autn) else { panic!() };
assert!(!old.resync(v.rand, (auts.0, auts.1 ^ 1))); // forged AUTS rejected
assert!(old.resync(v.rand, auts));
let v = old.vector(3001);
assert_eq!(card.challenge(v.rand, v.autn), Out::Ok(v.xres));
// ns per card-side check, success path
let (v, n) = (home.vector(9), 2_000_000u32);
let (t, c0) = (Instant::now(), Card { arr: [0; 4], top: 0, ..card });
for _ in 0..n { let mut c = c0; black_box(c.challenge(black_box(v.rand), black_box(v.autn))); }
println!("{:.1} ns per check", t.elapsed().as_nanos() as f64 / n as f64);
}
Complexity and constants
- Home per vector and card per success: f1 to f5 (Milenage: 5 AES-128 blocks with OPc stored, read off the TS 35.206 equations: TEMP, OUT1 to OUT4). A bad MAC costs the card 3 blocks.
- One home request per batch, one challenge and one response per login; a resync adds a home round trip.
- Toy functions: 8.5 ns per card-side check (3 toy calls, success path, M4 Pro, release build). Real cards are bound by the secure element.
- Wire: RAND 16 B, AUTN 16 B, RES 4-16 B.
Efficiency
- Cost: about 5 AES blocks per login on card and home; the real cost is the home round trip and the SQN state. Bound: latency to home.
- Levers:
- Vector batches — one home round trip serves n logins (TS 33.102; 5G AKA does not pre-fetch, TS 33.501) — UMTS and LTE style cores.
- OPc stored, not recomputed — one AES block per call; TS 35.206 recommends computing it off the card — personalisation.
- MAC check before the SQN check — a forged challenge costs least — constrained devices.
- SQN array with indices — fewer resync round trips when vectors arrive out of order — several serving nodes per subscriber.
- Naive → standard → state of the art:
- Challenge-response, network never proven · fake network possible
- MAC but no SQN · replayable
- AKA with SQN array, AUTS resync, home confirmation (5G) · 1 home round trip per batch or login
Implementation notes
- Draw RAND from a CSPRNG. Compare MAC and RES in constant time (the toy code does not).
- Keep K and OPc in an HSM at home; personalisation files hold every K.
- Persist SQN_HE before releasing vectors; a restore from backup yields stale vectors (the resync path in the code).
- A vector is used once; the exception is retransmitting it while no response arrived (TS 33.102 clause 6.3.3).
- 5G derives K_AUSF, then K_SEAF (binds the serving network name) with a KDF (TS 33.501 Annex A).
Pitfalls
- AK conceals SQN only against passive attacks (TS 33.102 clause 6.3.2); a replayed challenge makes the card reuse the same AK for different SQN_MS, leaking SQN bits via AUTS. Annex J (informative) adds MAC-S to f5** to prevent it.
- 5G: the device checks AMF bit 0 in AUTN is 1; the null SUCI scheme leaves the identity unconcealed.
- GSM AKA authenticates the SIM to the network only (TS 33.102 definitions); nothing proves the network to the SIM, the gap a fake base station uses.
- No resync path: a restored home counter locks subscribers out.
Variants and alternatives
In production
- Cellular networks from GSM to 5G, with SIM, USIM and secure elements.
- Trusted and untrusted non-3GPP (Wi-Fi) access to the 5G core (TS 33.501 clauses 7, 7A).
- Standalone non-public networks: AKA-based or other methods such as EAP-TLS (TS 33.501 Annex I).
- Open-source cores (free5GC, Open5GS), RADIUS servers (FreeRADIUS) and supplicants (wpa_supplicant) implement the flow.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| open5gs | C | open5gs/open5gs | — | — | — | — | — | open-source mobile core network |
| free5GC | Go | free5gc/free5gc | — | — | — | — | — | open-source 5G core, Linux Foundation project |
| hostap (wpa_supplicant) | C | w1.fi/wpa_supplicant | — | — | — | — | — | EAP peer method for EAP-AKA and EAP-AKA' |
| FreeRADIUS | C | FreeRADIUS/freeradius-server | — | — | — | — | — | EAP-AKA' server module |
| milenage | Rust | olivierlemoal/milenage-rs | cargo add milenage | — | — | — | — | MILENAGE algorithm set |
Sources
- 3GPP, ETSI TS 133 102 V19.1.0 (2026-01), 3G security; Security architecture. pdf
- 3GPP, ETSI TS 133 501 V19.7.0 (2026-08), Security architecture and procedures for 5G System. pdf
- 3GPP, ETSI TS 135 206 V19.0.0 (2025-10), Specification of the MILENAGE algorithm set, Document 2: Algorithm specification. pdf
- 3GPP, ETSI TS 135 231 V19.0.0 (2026-01), Specification of the TUAK algorithm set, Document 1: Algorithm specification. pdf
- Arkko, Haverinen, Extensible Authentication Protocol Method for 3rd Generation Authentication and Key Agreement (EAP-AKA), RFC 4187, 2006. rfc
- Arkko, Lehtovirta, Torvinen, Eronen, Improved Extensible Authentication Protocol Method for 3GPP Mobile Network Authentication and Key Agreement (EAP-AKA'), RFC 9048, 2021. rfc