Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a7caceb1b0 |
|||
|
f3cd70d05e |
|||
|
729ccaa01d |
5 changed files with 178 additions and 26 deletions
66
AGENTS.md
66
AGENTS.md
|
|
@ -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
|
||||
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
|
||||
test reaches — some older than 0.3.10, some added by 0.3.10 or 0.3.11: the reg-event
|
||||
test reaches — some older than 0.3.10, some added by 0.3.10, 0.3.11 or 0.3.12: the reg-event
|
||||
subscription schedule (§7.4: its retry
|
||||
ladder, Retry-After clamp, Timer F, renewal point and RFC 6665 reasons); the
|
||||
registration sequence and its transactions (`AwaitFinal`/`AwaitInitialFinal`:
|
||||
|
|
@ -82,7 +82,10 @@ 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
|
||||
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
|
||||
as a refusal carrying its status); the
|
||||
as a refusal carrying its status); the state-file round trip
|
||||
(`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
|
||||
refusals and network silence, and the catch sites that sort a failed refresh
|
||||
(the keepalive's, `ReconnectCycle`'s) and a failed fresh attempt
|
||||
|
|
@ -261,6 +264,23 @@ Rules:
|
|||
bench — and O2 has not run this shape) and
|
||||
the escalation by one refused refresh that carries a `Retry-After` — the
|
||||
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
|
||||
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
|
||||
|
|
@ -427,10 +447,20 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
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).
|
||||
A registration resumed from the kernel's SAs sends the same
|
||||
Authorization and Security-Client, but its Security-Verify — on the
|
||||
resume's re-REGISTER and on every keepalive after it, until its next
|
||||
fresh registration — is the one entry rebuilt from the SA
|
||||
(`Imsd:Ipsec`), not the 401's list, as before 0.3.11. A 401 to a
|
||||
Security-Client as that refresh, the Authorization the state file holds
|
||||
(a file from before 0.3.11: the empty digest) and the Security-Verify
|
||||
from the state file (since 0.3.12); resumed from a file written before
|
||||
0.3.12, from one whose list `PersistState` wrote empty, or from one
|
||||
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
|
||||
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
|
||||
|
|
@ -438,7 +468,13 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
in-use SPIs) was refused with 503 (§13).
|
||||
|
||||
The state file is JSON: `callid`, `ftag`, `cseq`, `spi_uc`, `spi_us`,
|
||||
`expiry`, `route`, `ppi`, `aor`, `contact_user`, `transport`, and since
|
||||
`expiry`, `route`, `ppi`, `aor`, `contact_user`, `transport`, since 0.3.12
|
||||
`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
|
||||
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,
|
||||
|
|
@ -746,7 +782,12 @@ Lines the operator (and the scripts) look for, all prefixed `imsd: `:
|
|||
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
|
||||
of it carries the empty digest until its next fresh registration, and
|
||||
the line repeats at each later resume of it.
|
||||
the line repeats at each later resume of it; `Security-Verify from the
|
||||
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 series ends unanswered.
|
||||
|
||||
|
|
@ -978,6 +1019,12 @@ only the imsd side of its bus choice.
|
|||
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
|
||||
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:`,
|
||||
`Co-Authored-By:` or similar lines, whoever or whatever drafted them.
|
||||
- **Signed commits.** Every commit is GPG-signed with the repository's
|
||||
|
|
@ -1010,7 +1057,8 @@ only the imsd side of its bus choice.
|
|||
something else.
|
||||
- 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 —
|
||||
nonce, cnonce and response. Dumps are mode 0600 under the state dir, off by default
|
||||
nonce, cnonce and response — and (since 0.3.12) the P-CSCF's
|
||||
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.
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import ImsdBus;
|
|||
namespace {
|
||||
|
||||
// ---- static config (env-overridable, same knobs as imsd.py) ---------------
|
||||
constexpr const char* Version = "0.3.11";
|
||||
constexpr const char* Version = "0.3.12";
|
||||
constexpr const char* BusName = "net.catcrafts.IMS1";
|
||||
constexpr const char* ObjPath = "/net/catcrafts/IMS1";
|
||||
constexpr const char* Iface = "net.catcrafts.IMS1";
|
||||
|
|
@ -644,6 +644,14 @@ struct RegState {
|
|||
// state file; its reregistrations carry an empty digest, the pre-0.3.11
|
||||
// shape, until the next fresh registration.
|
||||
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
|
||||
|
|
@ -658,14 +666,21 @@ std::string StateDir() {
|
|||
// tiny JSON reader/writer for the persisted registration context.
|
||||
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) {
|
||||
std::string ss = r.securityServer.find_first_of("\"\\\n") == std::string::npos ? r.securityServer : std::string();
|
||||
std::string j = std::format(
|
||||
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
|
||||
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
|
||||
"\"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.contactUser, transport, r.authNonce, r.authCnonce, r.authResponse);
|
||||
r.contactUser, transport, r.authNonce, r.authCnonce, r.authResponse, ss);
|
||||
std::string path = StatePath();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
||||
|
|
@ -689,7 +704,7 @@ void DumpRaw(std::string_view name, std::string_view msg) {
|
|||
}
|
||||
|
||||
struct PersistedState {
|
||||
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport, authNonce, authCnonce, authResponse;
|
||||
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser, transport, authNonce, authCnonce, authResponse, securityServer;
|
||||
std::optional<long> cseq, spiUc, spiUs, expiry;
|
||||
};
|
||||
std::optional<PersistedState> LoadState() {
|
||||
|
|
@ -710,6 +725,7 @@ std::optional<PersistedState> LoadState() {
|
|||
ps.authNonce = QuotedAfter(s, "\"auth_nonce\": \""); // the three absent in pre-0.3.11 files
|
||||
ps.authCnonce = QuotedAfter(s, "\"auth_cnonce\": \"");
|
||||
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.spiUc = IntAfter(s, "\"spi_uc\": ");
|
||||
ps.spiUs = IntAfter(s, "\"spi_us\": ");
|
||||
|
|
@ -1280,7 +1296,29 @@ private:
|
|||
// No fallback identity: an empty ppi omits P-Preferred-Identity and
|
||||
// is re-learned from the next 200's P-Associated-URI.
|
||||
ppi_ = ps->ppi.value_or("");
|
||||
ss_ = sa->securityServer;
|
||||
// The Security-Verify the protected REGISTERs, the reg-event
|
||||
// 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
|
||||
// here so a call placed before the refresh 200 re-learns it does not
|
||||
// fall back to the barred temporary IMPU.
|
||||
|
|
@ -1308,6 +1346,7 @@ private:
|
|||
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_.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("");
|
||||
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);
|
||||
|
|
@ -1477,6 +1516,7 @@ private:
|
|||
reg_.contactUser = contactUser;
|
||||
reg_.spiUc = spiUc; reg_.spiUs = spiUs;
|
||||
reg_.authNonce = *nonceB64; reg_.authCnonce = cnonce; reg_.authResponse = response;
|
||||
reg_.securityServer = ss_;
|
||||
reg_.expiry = imsd::sip::GrantedExpires(ok).value_or(0);
|
||||
DumpRaw("imsd-register-200.raw", ok);
|
||||
UpdateRouteAndPpi(ok);
|
||||
|
|
@ -1572,9 +1612,9 @@ private:
|
|||
// successful authentication, a Security-Client with new SPIs and a new
|
||||
// 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
|
||||
// list for a registration this process made fresh; for a resumed one
|
||||
// the entry rebuilt from the kernel's SA, on every reregistration until
|
||||
// the next fresh registration). Returns (200 msg, used cseq). A 401
|
||||
// list, kept in the state file since 0.3.12 so a resumed registration
|
||||
// echoes it too; a resume from an older file sends the one entry
|
||||
// rebuilt from the kernel's SA). Returns (200 msg, used cseq). A 401
|
||||
// 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
|
||||
// fresh registration (RegRecovery's 401 rule). Every refresh of the
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ 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
|
||||
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
|
||||
`ip`; for a warm resume it reconstructs the SPIs, ports, and Security-Server
|
||||
value back out of `ip xfrm state`/`policy` text — the Security-Verify and
|
||||
the P-CSCF ports for the resume's flow, and the SPIs in use, which a
|
||||
reregistration's new SPIs must differ from (and the state file falls back
|
||||
to).
|
||||
`ip`; for a warm resume it reconstructs the SPIs and ports back out of `ip
|
||||
xfrm state`/`policy` text — the P-CSCF ports for the resume's flow, and the
|
||||
SPIs in use, which a reregistration's new SPIs must differ from (and the
|
||||
state file falls back to) — and a one-mechanism Security-Server value that
|
||||
stands in for the Security-Verify when the state file holds no list for the
|
||||
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
|
||||
shapes and the parser are pinned by tests/Ipsec.
|
||||
|
|
@ -308,7 +310,7 @@ export namespace imsd::ipsec {
|
|||
// What a warm SA in the kernel tells us — enough for a refresh re-REGISTER
|
||||
// to re-bind without re-authenticating.
|
||||
struct ExistingSa {
|
||||
std::string securityServer; // reconstructed Security-Server header value
|
||||
std::string securityServer; // one-mechanism Security-Server value: the resume's Security-Verify when the state file holds no list for this pair
|
||||
int portPs = 0;
|
||||
int portPc = 0;
|
||||
std::uint32_t spiUc = 0; // our inbound client SPI (a reregistration offers a different one)
|
||||
|
|
@ -403,6 +405,35 @@ export namespace imsd::ipsec {
|
|||
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
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ import :Sip;
|
|||
export namespace imsd::msg {
|
||||
|
||||
// Everything the builders need that is stable for the life of a
|
||||
// registration. Ports default to the fixed protected values; route/ppi/
|
||||
// securityServer are filled in from the REGISTER 200 OK.
|
||||
// registration. Ports default to the fixed protected values; route and
|
||||
// ppi are filled in from the REGISTER 200 OK, securityServer from the
|
||||
// 401 (at a resume, from the state file).
|
||||
struct Context {
|
||||
imsd::aka::Identity id;
|
||||
std::string local;
|
||||
|
|
@ -63,7 +64,7 @@ export namespace imsd::msg {
|
|||
// self-targeted requests like reg-event
|
||||
// SUBSCRIBE; the temporary (IMSI-derived)
|
||||
// IMPU is barred for anything but REGISTER
|
||||
std::string securityServer; // Security-Verify value (the network's ss)
|
||||
std::string securityServer; // Security-Verify value: the 401's Security-Server list (from the state file on a resume)
|
||||
// P-Access-Network-Info value (imsd::aka::ParsePani from live mmcli
|
||||
// location: real MCC/MNC/TAC/ECI, FDD). Empty omits the header —
|
||||
// never fabricate one: the network reads it to learn which access
|
||||
|
|
|
|||
|
|
@ -196,6 +196,38 @@ int main() {
|
|||
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");
|
||||
}
|
||||
// ---- 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
|
||||
{
|
||||
// spec-shaped list: every entry carries the SPI set, md5 preferred by q — we offered sha1, so sha1 wins (audit A1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue