Compare commits

..
5 changed files with 26 additions and 178 deletions

View file

@ -73,7 +73,7 @@ in every source but the re-exporting `Imsd.cppm`, clang + libc++ + lld.
The rule behind the split: **`imsd-core` stays GLib-free and free of I/O so The rule behind the split: **`imsd-core` stays GLib-free and free of I/O so
that every decision in it has a unit test.** New protocol logic goes into a that every decision in it has a unit test.** New protocol logic goes into a
partition with a test. The shell (`main.cpp`) holds protocol decisions no partition with a test. The shell (`main.cpp`) holds protocol decisions no
test reaches — some older than 0.3.10, some added by 0.3.10, 0.3.11 or 0.3.12: the reg-event test reaches — some older than 0.3.10, some added by 0.3.10 or 0.3.11: the reg-event
subscription schedule (§7.4: its retry subscription schedule (§7.4: its retry
ladder, Retry-After clamp, Timer F, renewal point and RFC 6665 reasons); the ladder, Retry-After clamp, Timer F, renewal point and RFC 6665 reasons); the
registration sequence and its transactions (`AwaitFinal`/`AwaitInitialFinal`: registration sequence and its transactions (`AwaitFinal`/`AwaitInitialFinal`:
@ -82,10 +82,7 @@ meanwhile and the routing of our reg-event SUBSCRIBE's final read there, the
12 s wait restarted on a provisional); the reregistration's headers and its 12 s wait restarted on a provisional); the reregistration's headers and its
answer to a challenge (`Reregister`: the stored challenge answer, or the empty answer to a challenge (`Reregister`: the stored challenge answer, or the empty
digest for a pre-0.3.11 state file, the new SPIs, `PortUcNext`; a 401 thrown digest for a pre-0.3.11 state file, the new SPIs, `PortUcNext`; a 401 thrown
as a refusal carrying its status); the state-file round trip as a refusal carrying its status); the
(`PersistState`/`LoadState`: the `security_server` key, written empty for a
value with a quote, a backslash or a newline — the choice between that list
and the entry rebuilt from the SA is `Imsd:Ipsec`'s, with a test); the
inputs to `RegRecovery` — `SipFlow::Connect`'s split of failures into local inputs to `RegRecovery` — `SipFlow::Connect`'s split of failures into local
refusals and network silence, and the catch sites that sort a failed refresh refusals and network silence, and the catch sites that sort a failed refresh
(the keepalive's, `ReconnectCycle`'s) and a failed fresh attempt (the keepalive's, `ReconnectCycle`'s) and a failed fresh attempt
@ -264,23 +261,6 @@ Rules:
bench — and O2 has not run this shape) and bench — and O2 has not run this shape) and
the escalation by one refused refresh that carries a `Retry-After` — the the escalation by one refused refresh that carries a `Retry-After` — the
escalation to a fresh registration is pinned in `tests/Engine` only; a escalation to a fresh registration is pinned in `tests/Engine` only; a
resume echoing a multi-mechanism Security-Server list on the wire (the
choice of the stored list is pinned in `tests/Ipsec` with O2's recorded
list; KPN's has one mechanism, so on the bench the stored value and the
rebuilt one are the same bytes; O2's six-entry list has not resumed on
this code, and the state-file round trip is shell code no test
reaches); a Security-Server list `PersistState` writes empty (one with
a quote, a backslash or a newline — neither KPN's nor O2's list has
one; the resume of such a file ran with a hand-edited empty key,
2026-10-11); a state write of a standing registration after a fresh
attempt that aborted before its SA setup (`RegState::securityServer`,
not the attempt's `ss_` — on KPN the old SA's retry after a throttled
attempt is refused 403, the throttled-attempt chain below, so no such
write follows here); the window that leaves a state file naming another
SA pair than the kernel's (a stop or a crash between a fresh attempt's
SA setup and its 200's state write — the check is pinned in `tests/Ipsec`
and ran on a hand-edited list, 2026-10-11; the window itself is not
staged); a
refused initial or fresh refused initial or fresh
REGISTER (403, 5xx) at bring-up or in recovery (KPN has only throttled or REGISTER (403, 5xx) at bring-up or in recovery (KPN has only throttled or
stayed silent — pinned in `tests/Engine` only); the write-off of the old stayed silent — pinned in `tests/Engine` only); the write-off of the old
@ -447,20 +427,10 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
45063) for the SA pairs a re-authentication would set up — the server 45063) for the SA pairs a re-authentication would set up — the server
port stays (TS 33.203 7.4.0) — and the Security-Verify (the 401's list). port stays (TS 33.203 7.4.0) — and the Security-Verify (the 401's list).
A registration resumed from the kernel's SAs sends the same A registration resumed from the kernel's SAs sends the same
Security-Client as that refresh, the Authorization the state file holds Authorization and Security-Client, but its Security-Verify — on the
(a file from before 0.3.11: the empty digest) and the Security-Verify resume's re-REGISTER and on every keepalive after it, until its next
from the state file (since 0.3.12); resumed from a file written before fresh registration — is the one entry rebuilt from the SA
0.3.12, from one whose list `PersistState` wrote empty, or from one (`Imsd:Ipsec`), not the 401's list, as before 0.3.11. A 401 to a
whose list names another SA pair than the kernel holds (an attempt the
process did not live to record: its SAs installed, its 200 not
written), it sends —
on the resume's re-REGISTER, the keepalive re-REGISTERs, the reg-event
SUBSCRIBE and INVITEs, until its next fresh registration — the one
entry rebuilt from the SA
(`Imsd:Ipsec`, which makes the choice): KPN's one-entry answer to the
default offer in the same bytes, not a list such as O2's six, which RFC 3329 §2.3.1 has the server answer with a
494 when it does not mirror its own; a refused resume registers fresh.
A 401 to a
reregistration asks for re-authentication over temporary SAs on the reregistration asks for re-authentication over temporary SAs on the
proposed ports, which imsd does not implement: it is a refusal and the proposed ports, which imsd does not implement: it is a refusal and the
recovery is a fresh registration (§7.4). Every refresh of the pre-0.3.11 recovery is a fresh registration (§7.4). Every refresh of the pre-0.3.11
@ -468,13 +438,7 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
in-use SPIs) was refused with 503 (§13). in-use SPIs) was refused with 503 (§13).
The state file is JSON: `callid`, `ftag`, `cseq`, `spi_uc`, `spi_us`, The state file is JSON: `callid`, `ftag`, `cseq`, `spi_uc`, `spi_us`,
`expiry`, `route`, `ppi`, `aor`, `contact_user`, `transport`, since 0.3.12 `expiry`, `route`, `ppi`, `aor`, `contact_user`, `transport`, and since
`security_server` (the registration's Security-Verify: the 401's list,
written empty when it holds a quote, a backslash or a newline; absent,
empty or naming another SA pair than the kernel's — a file from before
0.3.12, such a list, or a file an unrecorded attempt left behind — the
resume rebuilds one mechanism from the SA, logs which, and stores that),
and since
0.3.11 `auth_nonce`, `auth_cnonce`, `auth_response` (a registration resumed 0.3.11 `auth_nonce`, `auth_cnonce`, `auth_response` (a registration resumed
from an older file has none: its reregistrations — the resume's and the from an older file has none: its reregistrations — the resume's and the
keepalives', across restarts — carry the empty digest, as before 0.3.11, keepalives', across restarts — carry the empty digest, as before 0.3.11,
@ -782,12 +746,7 @@ Lines the operator (and the scripts) look for, all prefixed `imsd: `:
no challenge answer (pre-0.3.11): the refresh carries an empty digest` no challenge answer (pre-0.3.11): the refresh carries an empty digest`
at a resume of a registration made before 0.3.11 — every reregistration at a resume of a registration made before 0.3.11 — every reregistration
of it carries the empty digest until its next fresh registration, and of it carries the empty digest until its next fresh registration, and
the line repeats at each later resume of it; `Security-Verify from the the line repeats at each later resume of it.
state file (1 mechanism)` / `(N mechanisms)`, `state file holds no Security-Server (a
pre-0.3.12 file, or a list the file cannot hold): Security-Verify
rebuilt from the SA as one mechanism` or `state file's Security-Server
names another SA pair than the one in the kernel (spi-s N vs M):
Security-Verify rebuilt from the SA as one mechanism` at every resume.
- `connect: no answer from the P-CSCF N times (…)` when the protected - `connect: no answer from the P-CSCF N times (…)` when the protected
connect series ends unanswered. connect series ends unanswered.
@ -1019,12 +978,6 @@ only the imsd side of its bus choice.
behaviour, in one sentence how it was tested. A version bump is a commit behaviour, in one sentence how it was tested. A version bump is a commit
titled `imsd X.Y.Z` that changes only the `Version` constant and has no titled `imsd X.Y.Z` that changes only the `Version` constant and has no
body. body.
- **Versioning.** `0.MINOR.PATCH`: the patch number counts releases, fixes
and features alike; the minor number moves when a release changes what
a user can rely on the daemon for (0.3 added emergency calling), not
with the size of a release; the major number stays 0 — what 1.0 would
mark is not decided, and it is not an ABI change (§8.1: the ABI is
frozen).
- **No AI attribution trailers.** Commits carry no `Assisted-by:`, - **No AI attribution trailers.** Commits carry no `Assisted-by:`,
`Co-Authored-By:` or similar lines, whoever or whatever drafted them. `Co-Authored-By:` or similar lines, whoever or whatever drafted them.
- **Signed commits.** Every commit is GPG-signed with the repository's - **Signed commits.** Every commit is GPG-signed with the repository's
@ -1057,8 +1010,7 @@ only the imsd side of its bus choice.
something else. something else.
- SIP dumps and the state file carry the IMSI, the MSISDN, SPIs and - SIP dumps and the state file carry the IMSI, the MSISDN, SPIs and
addresses, and the state file (since 0.3.11) the last challenge answer — addresses, and the state file (since 0.3.11) the last challenge answer —
nonce, cnonce and response — and (since 0.3.12) the P-CSCF's nonce, cnonce and response. Dumps are mode 0600 under the state dir, off by default
Security-Server list. Dumps are mode 0600 under the state dir, off by default
(`DUMP_SIP=0`), and never leave the phone in a commit. (`DUMP_SIP=0`), and never leave the phone in a commit.
- The ims PDN is a walled garden: `ims-pdn-up.sh` keeps it from becoming - The ims PDN is a walled garden: `ims-pdn-up.sh` keeps it from becoming
the default route and gives the P-CSCF a host route; imsd adds one per the default route and gives the P-CSCF a host route; imsd adds one per

View file

@ -48,7 +48,7 @@ import ImsdBus;
namespace { namespace {
// ---- static config (env-overridable, same knobs as imsd.py) --------------- // ---- static config (env-overridable, same knobs as imsd.py) ---------------
constexpr const char* Version = "0.3.12"; constexpr const char* Version = "0.3.11";
constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* BusName = "net.catcrafts.IMS1";
constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1";
constexpr const char* Iface = "net.catcrafts.IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1";
@ -644,14 +644,6 @@ struct RegState {
// state file; its reregistrations carry an empty digest, the pre-0.3.11 // state file; its reregistrations carry an empty digest, the pre-0.3.11
// shape, until the next fresh registration. // shape, until the next fresh registration.
std::string authNonce, authCnonce, authResponse; std::string authNonce, authCnonce, authResponse;
// The Security-Verify this registration's protected REGISTERs, its
// reg-event SUBSCRIBE and its INVITEs echo: the 401's whole
// Security-Server list (RFC 3329 §2.3.1). Set on the fresh 200 and at
// the resume, never from an attempt in flight:
// the member ss_ holds a fresh attempt's 401 before that attempt can
// still abort (the throttle, an unsupported mechanism), and the
// standing registration's state writes must not store it.
std::string securityServer;
}; };
// Writable directory for the daemon's own files: the persisted registration // Writable directory for the daemon's own files: the persisted registration
@ -666,21 +658,14 @@ std::string StateDir() {
// tiny JSON reader/writer for the persisted registration context. // tiny JSON reader/writer for the persisted registration context.
std::string StatePath() { return EnvOr("STATE_FILE", std::format("{}/imsreg.state", StateDir())); } std::string StatePath() { return EnvOr("STATE_FILE", std::format("{}/imsreg.state", StateDir())); }
// `security_server` is the registration's Security-Verify (RegState's
// securityServer: the 401's joined list); a resume takes it from here
// since 0.3.12. The file's values are quoted without escaping; a value
// with a quote, a backslash or a newline in it (none RFC 3329's token
// syntax produces) is written empty, and the resume falls back to the SA.
void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor, const std::string& transport) { void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor, const std::string& transport) {
std::string ss = r.securityServer.find_first_of("\"\\\n") == std::string::npos ? r.securityServer : std::string();
std::string j = std::format( std::string j = std::format(
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, " "{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", " "\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
"\"aor\": \"{}\", \"contact_user\": \"{}\", \"transport\": \"{}\", " "\"aor\": \"{}\", \"contact_user\": \"{}\", \"transport\": \"{}\", "
"\"auth_nonce\": \"{}\", \"auth_cnonce\": \"{}\", \"auth_response\": \"{}\", " "\"auth_nonce\": \"{}\", \"auth_cnonce\": \"{}\", \"auth_response\": \"{}\"}}",
"\"security_server\": \"{}\"}}",
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor, r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor,
r.contactUser, transport, r.authNonce, r.authCnonce, r.authResponse, ss); r.contactUser, transport, r.authNonce, r.authCnonce, r.authResponse);
std::string path = StatePath(); std::string path = StatePath();
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
@ -704,7 +689,7 @@ void DumpRaw(std::string_view name, std::string_view msg) {
} }
struct PersistedState { struct PersistedState {
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport, authNonce, authCnonce, authResponse, securityServer; std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport, authNonce, authCnonce, authResponse;
std::optional<long> cseq, spiUc, spiUs, expiry; std::optional<long> cseq, spiUc, spiUs, expiry;
}; };
std::optional<PersistedState> LoadState() { std::optional<PersistedState> LoadState() {
@ -725,7 +710,6 @@ std::optional<PersistedState> LoadState() {
ps.authNonce = QuotedAfter(s, "\"auth_nonce\": \""); // the three absent in pre-0.3.11 files ps.authNonce = QuotedAfter(s, "\"auth_nonce\": \""); // the three absent in pre-0.3.11 files
ps.authCnonce = QuotedAfter(s, "\"auth_cnonce\": \""); ps.authCnonce = QuotedAfter(s, "\"auth_cnonce\": \"");
ps.authResponse = QuotedAfter(s, "\"auth_response\": \""); ps.authResponse = QuotedAfter(s, "\"auth_response\": \"");
ps.securityServer = QuotedAfter(s, "\"security_server\": \""); // absent in pre-0.3.12 files
ps.cseq = IntAfter(s, "\"cseq\": "); ps.cseq = IntAfter(s, "\"cseq\": ");
ps.spiUc = IntAfter(s, "\"spi_uc\": "); ps.spiUc = IntAfter(s, "\"spi_uc\": ");
ps.spiUs = IntAfter(s, "\"spi_us\": "); ps.spiUs = IntAfter(s, "\"spi_us\": ");
@ -1296,29 +1280,7 @@ private:
// No fallback identity: an empty ppi omits P-Preferred-Identity and // No fallback identity: an empty ppi omits P-Preferred-Identity and
// is re-learned from the next 200's P-Associated-URI. // is re-learned from the next 200's P-Associated-URI.
ppi_ = ps->ppi.value_or(""); ppi_ = ps->ppi.value_or("");
// The Security-Verify the protected REGISTERs, the reg-event ss_ = sa->securityServer;
// SUBSCRIBE and the INVITEs echo: the 401's whole Security-Server
// list (RFC 3329 §2.3.1), from the state file since 0.3.12, or the
// one-mechanism entry rebuilt from the SA — Imsd:Ipsec chooses
// (ChooseResumeSecurityVerify). The rebuilt entry is KPN's answer to
// the default offer in the same bytes, not a list such as O2's six,
// which the RFC has the server answer with a 494; a refused resume
// registers fresh.
auto rsv = imsd::ipsec::ChooseResumeSecurityVerify(ps->securityServer.value_or(""), *sa, ealgOffer_);
ss_ = rsv.value;
using Source = imsd::ipsec::ResumeSecurityVerify::Source;
switch (rsv.source) {
case Source::Stored:
Log(std::format("Security-Verify from the state file ({} mechanism{})", rsv.mechanisms, rsv.mechanisms == 1 ? "" : "s"));
break;
case Source::NoneStored:
Log("state file holds no Security-Server (a pre-0.3.12 file, or a list the file cannot hold): Security-Verify rebuilt from the SA as one mechanism");
break;
case Source::OtherSa:
Log(std::format("state file's Security-Server names another SA pair than the one in the kernel (spi-s {} vs {}): Security-Verify rebuilt from the SA as one mechanism",
rsv.storedSpiPs ? std::to_string(*rsv.storedSpiPs) : "none", sa->spiPs));
break;
}
// The public identity our requests are signed with (From) — restored // The public identity our requests are signed with (From) — restored
// here so a call placed before the refresh 200 re-learns it does not // here so a call placed before the refresh 200 re-learns it does not
// fall back to the barred temporary IMPU. // fall back to the barred temporary IMPU.
@ -1346,7 +1308,6 @@ private:
reg_.spiUc = static_cast<std::uint32_t>(ps->spiUc.value_or(sa->spiUc)); reg_.spiUc = static_cast<std::uint32_t>(ps->spiUc.value_or(sa->spiUc));
reg_.spiUs = static_cast<std::uint32_t>(ps->spiUs.value_or(sa->spiUs)); reg_.spiUs = static_cast<std::uint32_t>(ps->spiUs.value_or(sa->spiUs));
reg_.expiry = ps->expiry.value_or(0); reg_.expiry = ps->expiry.value_or(0);
reg_.securityServer = ss_;
reg_.authNonce = ps->authNonce.value_or(""); reg_.authCnonce = ps->authCnonce.value_or(""); reg_.authResponse = ps->authResponse.value_or(""); reg_.authNonce = ps->authNonce.value_or(""); reg_.authCnonce = ps->authCnonce.value_or(""); reg_.authResponse = ps->authResponse.value_or("");
if (reg_.authNonce.empty()) Log("state file holds no challenge answer (pre-0.3.11): the refresh carries an empty digest"); if (reg_.authNonce.empty()) Log("state file holds no challenge answer (pre-0.3.11): the refresh carries an empty digest");
auto [okMsg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1); auto [okMsg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1);
@ -1516,7 +1477,6 @@ private:
reg_.contactUser = contactUser; reg_.contactUser = contactUser;
reg_.spiUc = spiUc; reg_.spiUs = spiUs; reg_.spiUc = spiUc; reg_.spiUs = spiUs;
reg_.authNonce = *nonceB64; reg_.authCnonce = cnonce; reg_.authResponse = response; reg_.authNonce = *nonceB64; reg_.authCnonce = cnonce; reg_.authResponse = response;
reg_.securityServer = ss_;
reg_.expiry = imsd::sip::GrantedExpires(ok).value_or(0); reg_.expiry = imsd::sip::GrantedExpires(ok).value_or(0);
DumpRaw("imsd-register-200.raw", ok); DumpRaw("imsd-register-200.raw", ok);
UpdateRouteAndPpi(ok); UpdateRouteAndPpi(ok);
@ -1612,9 +1572,9 @@ private:
// successful authentication, a Security-Client with new SPIs and a new // successful authentication, a Security-Client with new SPIs and a new
// client port for the SA pairs a re-authentication would set up (the // client port for the SA pairs a re-authentication would set up (the
// server port stays, TS 33.203 7.4.0), the Security-Verify (the 401's // server port stays, TS 33.203 7.4.0), the Security-Verify (the 401's
// list, kept in the state file since 0.3.12 so a resumed registration // list for a registration this process made fresh; for a resumed one
// echoes it too; a resume from an older file sends the one entry // the entry rebuilt from the kernel's SA, on every reregistration until
// rebuilt from the kernel's SA). Returns (200 msg, used cseq). A 401 // the next fresh registration). Returns (200 msg, used cseq). A 401
// asks for re-authentication over temporary SAs on the proposed ports, // asks for re-authentication over temporary SAs on the proposed ports,
// which imsd does not do: it is thrown as a refusal whose recovery is a // which imsd does not do: it is thrown as a refusal whose recovery is a
// fresh registration (RegRecovery's 401 rule). Every refresh of the // fresh registration (RegRecovery's 401 rule). Every refresh of the

View file

@ -13,13 +13,11 @@ alg (HMAC-SHA-1-96 or HMAC-MD5-96) keyed with IK; confidentiality is the
negotiated ealg keyed with CK. Both come from the Security-Server mechanism negotiated ealg keyed with CK. Both come from the Security-Server mechanism
the P-CSCF selected in its 401 (parsed here), and this module turns the the P-CSCF selected in its 401 (parsed here), and this module turns the
negotiated SPIs/ports/keys into the exact argv lists the daemon shell hands to negotiated SPIs/ports/keys into the exact argv lists the daemon shell hands to
`ip`; for a warm resume it reconstructs the SPIs and ports back out of `ip `ip`; for a warm resume it reconstructs the SPIs, ports, and Security-Server
xfrm state`/`policy` text — the P-CSCF ports for the resume's flow, and the value back out of `ip xfrm state`/`policy` text — the Security-Verify and
SPIs in use, which a reregistration's new SPIs must differ from (and the the P-CSCF ports for the resume's flow, and the SPIs in use, which a
state file falls back to) — and a one-mechanism Security-Server value that reregistration's new SPIs must differ from (and the state file falls back
stands in for the Security-Verify when the state file holds no list for the to).
SA pair in the kernel (`ChooseResumeSecurityVerify` decides; since 0.3.12
the 401's own list is stored there).
Pure std C++ (the shell runs the commands and captures the text). Command Pure std C++ (the shell runs the commands and captures the text). Command
shapes and the parser are pinned by tests/Ipsec. shapes and the parser are pinned by tests/Ipsec.
@ -310,7 +308,7 @@ export namespace imsd::ipsec {
// What a warm SA in the kernel tells us — enough for a refresh re-REGISTER // What a warm SA in the kernel tells us — enough for a refresh re-REGISTER
// to re-bind without re-authenticating. // to re-bind without re-authenticating.
struct ExistingSa { struct ExistingSa {
std::string securityServer; // one-mechanism Security-Server value: the resume's Security-Verify when the state file holds no list for this pair std::string securityServer; // reconstructed Security-Server header value
int portPs = 0; int portPs = 0;
int portPc = 0; int portPc = 0;
std::uint32_t spiUc = 0; // our inbound client SPI (a reregistration offers a different one) std::uint32_t spiUc = 0; // our inbound client SPI (a reregistration offers a different one)
@ -405,35 +403,6 @@ export namespace imsd::ipsec {
return sa; return sa;
} }
// The Security-Verify a resume sends, and where it came from: the state
// file's list when the entry carrying its SPIs names the SA pair the
// kernel holds (a file an attempt the process did not live to record
// left behind names another pair), else the one-mechanism entry rebuilt
// from the SA — for an empty or unparseable value too.
struct ResumeSecurityVerify {
enum class Source { Stored, NoneStored, OtherSa };
Source source = Source::NoneStored;
std::string value;
std::size_t mechanisms = 0; // of the stored list, when it is used
std::optional<std::uint32_t> storedSpiPs; // the stored list's spi-s (OtherSa); nullopt = none parsed
};
ResumeSecurityVerify ChooseResumeSecurityVerify(std::string_view stored, const ExistingSa& sa, std::string_view offeredEalg) {
ResumeSecurityVerify r;
r.value = sa.securityServer;
if (stored.empty()) return r;
r.source = ResumeSecurityVerify::Source::OtherSa;
if (auto parsed = ParseSecurityServer(stored, "hmac-sha-1-96", offeredEalg)) {
const SecurityMechanism& c = parsed->Carrier();
r.storedSpiPs = c.spiPs;
if (c.spiPc == sa.spiPc && c.spiPs == sa.spiPs && c.portPc == sa.portPc && c.portPs == sa.portPs) {
r.source = ResumeSecurityVerify::Source::Stored;
r.value = std::string(stored);
r.mechanisms = parsed->mechanisms.size();
}
}
return r;
}
// Every local address holding an SA to the P-CSCF, once each, in kernel // Every local address holding an SA to the P-CSCF, once each, in kernel
// order: where a registration made on another address of the link (the // order: where a registration made on another address of the link (the
// kernel's temporary one, before 0.3.10 — ims ledger F18) still lives. // kernel's temporary one, before 0.3.10 — ims ledger F18) still lives.

View file

@ -29,9 +29,8 @@ import :Sip;
export namespace imsd::msg { export namespace imsd::msg {
// Everything the builders need that is stable for the life of a // Everything the builders need that is stable for the life of a
// registration. Ports default to the fixed protected values; route and // registration. Ports default to the fixed protected values; route/ppi/
// ppi are filled in from the REGISTER 200 OK, securityServer from the // securityServer are filled in from the REGISTER 200 OK.
// 401 (at a resume, from the state file).
struct Context { struct Context {
imsd::aka::Identity id; imsd::aka::Identity id;
std::string local; std::string local;
@ -64,7 +63,7 @@ export namespace imsd::msg {
// self-targeted requests like reg-event // self-targeted requests like reg-event
// SUBSCRIBE; the temporary (IMSI-derived) // SUBSCRIBE; the temporary (IMSI-derived)
// IMPU is barred for anything but REGISTER // IMPU is barred for anything but REGISTER
std::string securityServer; // Security-Verify value: the 401's Security-Server list (from the state file on a resume) std::string securityServer; // Security-Verify value (the network's ss)
// P-Access-Network-Info value (imsd::aka::ParsePani from live mmcli // P-Access-Network-Info value (imsd::aka::ParsePani from live mmcli
// location: real MCC/MNC/TAC/ECI, FDD). Empty omits the header — // location: real MCC/MNC/TAC/ECI, FDD). Empty omits the header —
// never fabricate one: the network reads it to learn which access // never fabricate one: the network reads it to learn which access

View file

@ -196,38 +196,6 @@ int main() {
auto bare = ParseSecurityServer("ipsec-3gpp; q=0.1; alg=hmac-sha-1-96; ealg=null;", "hmac-sha-1-96", "null"); auto bare = ParseSecurityServer("ipsec-3gpp; q=0.1; alg=hmac-sha-1-96; ealg=null;", "hmac-sha-1-96", "null");
Check(bare && bare->mechanisms.size() == 1 && bare->offerMatched && !bare->Carrier().spiPs && !bare->Carrier().portPs, "no spi-s: absent, not zero"); Check(bare && bare->mechanisms.size() == 1 && bare->offerMatched && !bare->Carrier().spiPs && !bare->Carrier().portPs, "no spi-s: absent, not zero");
} }
// ---- the resume's Security-Verify: the stored list only for the SA pair the kernel holds
{
using Source = ResumeSecurityVerify::Source;
ExistingSa sa;
sa.spiPc = 129427852u; sa.spiPs = 133937015u; sa.portPc = 6051; sa.portPs = 6060;
sa.securityServer = "ipsec-3gpp; q=0.1; alg=hmac-sha-1-96; ealg=aes-cbc; spi-c=129427852; spi-s=133937015; port-c=6051; port-s=6060";
auto ok = ChooseResumeSecurityVerify(O2, sa, "null");
Check(ok.source == Source::Stored && ok.value == O2 && ok.mechanisms == 6, "resume: O2's six-entry list whose carrier is the kernel's pair -> the list verbatim, six mechanisms");
auto okAes = ChooseResumeSecurityVerify(O2, sa, "aes-cbc");
Check(okAes.source == Source::Stored && okAes.value == O2, "resume: the same under the aes offer (the carrier does not depend on the offer)");
constexpr std::pair<std::string_view, std::string_view> off[] = {
{"spi-c", "ipsec-3gpp;q=0.94;alg=hmac-sha-1-96;mod=trans;spi-c=129427853;spi-s=133937015;port-c=6051;port-s=6060"},
{"spi-s", "ipsec-3gpp;q=0.94;alg=hmac-sha-1-96;mod=trans;spi-c=129427852;spi-s=133937016;port-c=6051;port-s=6060"},
{"port-c", "ipsec-3gpp;q=0.94;alg=hmac-sha-1-96;mod=trans;spi-c=129427852;spi-s=133937015;port-c=6052;port-s=6060"},
{"port-s", "ipsec-3gpp;q=0.94;alg=hmac-sha-1-96;mod=trans;spi-c=129427852;spi-s=133937015;port-c=6051;port-s=6061"},
};
for (const auto& [field, text] : off) {
auto r = ChooseResumeSecurityVerify(text, sa, "null");
Check(r.source == Source::OtherSa && r.value == sa.securityServer, std::format("resume: {} off by one -> the rebuilt entry", field));
}
auto offSpiS = ChooseResumeSecurityVerify(off[1].second, sa, "null");
Check(offSpiS.storedSpiPs && *offSpiS.storedSpiPs == 133937016u, "resume: the stored spi-s is reported for the log line");
auto none = ChooseResumeSecurityVerify("", sa, "null");
Check(none.source == Source::NoneStored && none.value == sa.securityServer, "resume: nothing stored -> the rebuilt entry");
auto junk = ChooseResumeSecurityVerify("not a list", sa, "null");
Check(junk.source == Source::OtherSa && junk.value == sa.securityServer && !junk.storedSpiPs, "resume: an unparseable value -> the rebuilt entry, no spi-s to report");
ExistingSa kpn;
kpn.spiPc = 49889723u; kpn.spiPs = 49889722u; kpn.portPc = 33142; kpn.portPs = 6000;
kpn.securityServer = "ipsec-3gpp; q=0.1; alg=hmac-sha-1-96; ealg=aes-cbc; spi-c=49889723; spi-s=49889722; port-c=33142; port-s=6000";
auto k = ChooseResumeSecurityVerify(kpn.securityServer, kpn, "aes-cbc");
Check(k.source == Source::Stored && k.value == kpn.securityServer && k.mechanisms == 1, "resume: KPN's one-entry list for its pair -> the list, one mechanism");
}
// ---- selection rules and syntax corners // ---- selection rules and syntax corners
{ {
// spec-shaped list: every entry carries the SPI set, md5 preferred by q — we offered sha1, so sha1 wins (audit A1) // spec-shaped list: every entry carries the SPI set, md5 preferred by q — we offered sha1, so sha1 wins (audit A1)