Skip to content

transports: assert an SSH algorithm floor on the SFTP hop (#178) - #317

Closed
wshallwshall wants to merge 1 commit into
mainfrom
w3-sftp-cipher-floor
Closed

transports: assert an SSH algorithm floor on the SFTP hop (#178)#317
wshallwshall wants to merge 1 commit into
mainfrom
w3-sftp-cipher-floor

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

BACKLOG #178

The owner ruled #178 BUILD, scoped narrowly: the SSH/SFTP hop was the single transport hop without a
forward-secrecy floor assertion. The configurable allow-list stays deferred.

Handed over by the authoring session at a usage cutoff. Reported COMPLETE and verified there against a
live paramiko server.

NOT INDEPENDENTLY VERIFIED BY THE COORDINATOR -- pushed and routed to preserve the work off a local
ref, not merged. Run the standard battery before landing.

Leak-scanned before pushing: zero hits, negative control firing.

Deliberately NOT armed for auto-merge.

… TLS one (#178)

The SSH hop was the one transport hop with no cipher assertion. Measured 2026-08-10:
harden_cipher_suites is called at 7 sites across 5 files (the API/UI listener, MLLP in
both directions, both DICOM contexts, FTPS, and the SMTP alert sink), while
disabled_algorithms appeared 0 times anywhere in the package. Driven against a live
paramiko server, the SFTP client accepted hmac-md5, hmac-sha1 and 3des-cbc without
complaint -- a partner, or an on-path attacker steering the negotiation, could pick any
of them and nothing would say so.

config/ssh_policy.py is the missing assertion, and it is the TLS floor re-expressed
rather than a second opinion. The shipped TLS contexts resolve to 17 suites whose
measured properties are Kx in {ECDH, DH, any}, Enc in {AES, AESGCM, CHACHA20} and Mac in
{AEAD, SHA256, SHA384}; each cell maps to one SSH rule (forward-secret key exchange, no
64-bit-block or broken cipher, no MD5/SHA-1 MAC). Two of those three are properties
OpenSSL's default cipher string gives the TLS side for free and paramiko's defaults do
not, which is why the SSH side has to name them.

Two operations, deliberately separated. BELOW-floor names are disabled -- SSH negotiates
from the offer, so pruning the offer IS the enforcement, unlike TLS where the context
already resolves forward-secret and harden_cipher_suites only has to check it. Everything
surviving is asserted recognisable, so an unclassified algorithm raises at connect naming
itself rather than being quietly dropped and narrowing reachability with no signal.

Against paramiko 5.0.0 the floor prunes 5 of 24 offered names: 3des-cbc, hmac-md5,
hmac-md5-96, hmac-sha1, hmac-sha1-96. Every algorithm a current OpenSSH negotiates by
default survives, AES-CBC included -- dropping it would re-introduce on this hop exactly
the interop regression harden_cipher_suites measured and declined to take on TLS.

Fails closed without failing silently: a partner offering nothing above the floor is
refused with a permanent error naming the category, the algorithms refused, why they are
refused, and what to enable instead -- not a timeout and not a bare "Incompatible ssh
server (no acceptable macs)", which reads like a partner defect. paramiko stays lazily
imported behind the [sftp] extra: ssh_policy imports it never, the caller passes the
offer in, and an install without paramiko behaves exactly as it did.

No operator override, deliberately, so nothing is added to security_loosenings. Nothing
plausible needs loosening from this floor, and a switch nobody asked for is a second
posture by the back door. If a partner ever forces the question it belongs there as a
REQUIRED parameter, the way the TLS deviations do. The operator-configurable
cipher/KEX/MAC allow-list that is #178's other half stays DEMAND-GATED and unbuilt --
this module has no configuration surface at all.

Verified against a real key exchange rather than by attribute, because a floor asserted
only on a constructed object passes identically when disabled_algorithms never reaches
the connect call. tests/test_ssh_algorithm_floor.py stands up a live paramiko SSH server
on loopback: the three algorithms that connected before are each refused after, with the
message asserted, and a modern partner plus a paramiko-defaults partner both still
negotiate. The pure-policy half imports no SSH library and runs without the extra.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Closing this PR, not abandoning the work. The branch w3-sftp-cipher-floor (c8bb1638) stays on origin and nothing is lost.

Why it cannot go green. It cites #178 alongside engine code but is forbidden from editing docs/BACKLOG.md -- a single lane holds sole-writer on the ledger. So backlog-hygiene is unsatisfiable here by construction. The only ways to clear it are to strip the BACKLOG citations, which is dodging the gate, or to add banners asserting progress the code does not yet support. Neither is acceptable.

Why it should not merge yet. Adversarial verification found the kex screen tests endswith(("-sha1","-md5")), which is terminal position only, so GSS-API names carrying the hash mid-string rate ABOVE and connect -- proven by executing the seam. And no CI leg installs the [sftp] extra, so nothing can observe the enforcement half.

Where the record now lives. #320 has landed and amends the item(s) in the ledger -- partial, not closed, with the residuals named. The ledger tells the truth about this work whether or not the code lands, which is what makes closing this PR safe.

To resume: fix the finding on w3-sftp-cipher-floor, then land it as a train with a banner commit, the way the wave trains do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant