engine, messages, util, imsd: the reregistration carries the last challenge answer and new SA parameters

The keepalive refresh and the resume's re-REGISTER carried an initial
registration's first-REGISTER Authorization — an empty nonce and
response — in the second REGISTER's frame, with a Security-Client
naming the SPIs and ports in use. TS 24.229 5.1.1.4.2 has a
reregistration carry the Authorization
of the last successful authentication — the last received nonce and
the last calculated response — and a Security-Client with the new
parameter values for the two SA pairs a re-authentication would set
up; TS 33.203 7.4.0 keeps the server ports and changes the client
ports. O2 UK refused the refresh of the old shape with 503 on every
occasion on record (alyx, 2026-09-24 to 10-08, a stationary phone
included) and accepted the fresh registrations that followed; KPN
accepted the old shape without a challenge.

The fresh registration now records the 401's nonce, our cnonce and
the response in the registration state (auth_nonce, auth_cnonce,
auth_response in the state file); a reregistration sends them
verbatim, mints new SPIs and proposes PortUcNext (45063) as the
client port with the server port unchanged. A registration resumed from a
state file written before this change has no answer: its
reregistrations carry the empty digest, as before, until its next
fresh registration, and the resume says so. A 401 to a reregistration asks
for re-authentication over temporary SAs on the proposed ports (TS
24.229 5.1.1.5.1), which imsd does not implement: the refresh throws
it as a refusal and RegRecovery makes the next step the fresh
registration, replacing the in-place answer over the old SAs.

Tested with the Messages and Engine suites and on the dev phone (KPN)
with a build of this commit's tree: a resume from the previous state
file, a fresh registration, a resume that sent the stored answer,
three refreshes at a 120-s interval, an outgoing call to a bench line
and an incoming call's ring.
This commit is contained in:
Jorijn van der Graaf 2026-10-10 21:00:28 +02:00
commit 799fd1a802
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
8 changed files with 185 additions and 72 deletions

View file

@ -88,9 +88,15 @@ export namespace imsd::msg {
return std::format("Via: SIP/2.0/{} {};branch=z9hG4bK{};rport", transport, imsd::util::HostPort(c.local, port), branch);
}
// Security-Client value carrying our in-use client SPIs + protected ports.
// Security-Client value: the SPIs and protected ports offered for the SA
// pairs this REGISTER may set up — the in-use ones on an initial
// registration, new SPIs and a new client port on a reregistration
// (TS 24.229 5.1.1.4.2 d; TS 33.203 7.4.0: the server port stays).
inline std::string SecurityClient(const Context& c, std::uint32_t spiUc, std::uint32_t spiUs, int portUc, int portUs) {
return std::format("Security-Client: ipsec-3gpp; alg=hmac-sha-1-96; ealg={}; " "spi-c={}; spi-s={}; port-c={}; port-s={}", c.ealgOffer, spiUc, spiUs, portUc, portUs);
}
inline std::string SecurityClient(const Context& c, std::uint32_t spiUc, std::uint32_t spiUs) {
return std::format("Security-Client: ipsec-3gpp; alg=hmac-sha-1-96; ealg={}; " "spi-c={}; spi-s={}; port-c={}; port-s={}", c.ealgOffer, spiUc, spiUs, c.portUc, c.portUs);
return SecurityClient(c, spiUc, spiUs, c.portUc, c.portUs);
}
// Authorization line with an empty (unchallenged) digest — the first
@ -217,9 +223,17 @@ export namespace imsd::msg {
// The stock oracle sends it in BOTH REGISTERs (2026-07-21 capture,
// reg2 build at 19:30:44); an earlier reading that reg2 omits it was
// wrong and cost us all terminating delivery. `authLine` is a full
// Authorization line (AuthEmpty for the refresh's first try, AuthAka
// once challenged).
std::string BuildRegisterProtected(const Context& c, std::string_view callid, std::string_view ftag, std::string_view branch, int cseq, std::uint32_t spiUc, std::uint32_t spiUs, std::string_view authLine) {
// Authorization line: AuthAka with the challenge's answer for the
// initial registration's second REGISTER and, verbatim, for every
// reregistration after it (TS 24.229 5.1.1.4.2 a: the last received
// nonce, the last calculated response); AuthEmpty for a registration
// resumed from a pre-0.3.11 state file, on every reregistration until
// its next fresh registration. A
// reregistration also passes NEW SPIs and the client port it proposes
// for a re-authentication's SA pairs (`portUc`, TS 33.203 7.4.0: the
// server port stays); 0 offers the in-use client port, the initial
// registration's case.
std::string BuildRegisterProtected(const Context& c, std::string_view callid, std::string_view ftag, std::string_view branch, int cseq, std::uint32_t spiUc, std::uint32_t spiUs, std::string_view authLine, int portUc = 0) {
std::vector<std::string> l = {
std::format("REGISTER {} SIP/2.0", c.id.regUri),
ViaLine(c, c.transport, c.portUs, branch),
@ -233,7 +247,7 @@ export namespace imsd::msg {
std::string(authLine),
"Require: sec-agree",
"Proxy-Require: sec-agree",
SecurityClient(c, spiUc, spiUs),
SecurityClient(c, spiUc, spiUs, portUc ? portUc : c.portUc, c.portUs),
std::format("Security-Verify: {}", c.securityServer),
};
l.push_back("Supported: sec-agree, path");