francoismichel / ssh3

SSH3: faster and rich secure shell using HTTP/3, checkout our article here: https://arxiv.org/abs/2312.08396 and our Internet-Draft: https://datatracker.ietf.org/doc/draft-michel-ssh3/
https://arxiv.org/abs/2312.08396
Apache License 2.0
3.28k stars 83 forks source link

Static redirect URIs required by OIDC specification #140

Open almereyda opened 6 months ago

almereyda commented 6 months ago

The current implementation of the OIDC client in the ssh3 client binary spawns a local HTTP webserver at a random port with a random URL path fragment. This contradicts the OIDC specification, which expects a persistent and static (list of) redirect URI(s), even when the application is set to confidential.

https://github.com/francoismichel/ssh3/blob/20f2894426742ee2fe92e33fa3d2521be7677b20/auth/openid_connect.go#L46-L56

3.1.2.1. Authentication Request redirect_uri REQUIRED. Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. Also, if the Client is a native application, it MAY use the http scheme with localhost or the IP loopback literals 127.0.0.1 or [::1] as the hostname. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.

This way it is not possible to set up an OIDC client application with an ID and secret pair that works, e.g. with GitLab.

OIDC Provider:

Notes:

Server configuration:

~/.ssh3/authorized_identities
oidc 6f1379417c8a5cae738bc030e5ddc59ec352af02704351e182727fb0136fe1fe https://gitlab.example.com user@example.com

Client configuration:

~/.ssh3/oidc_config.json
[
    {
        "issuer_url": "https://gitlab.example.com",
        "client_id": "<Application ID from above>", // e.g. 6f1379417c8a5cae738bc030e5ddc59ec352af02704351e182727fb0136fe1fe
        "client_secret": "<Secret from above>" // e.g. gloas-48ac71b03fcf805a127ba614c88646956751e154c76b77991f909a918c21b86e
    }
]

Note: The Readme speaks of config.json and also oidc_config.json, while the latter is the default when not using the -oidc-config flag.

https://github.com/francoismichel/ssh3/blob/20f2894426742ee2fe92e33fa3d2521be7677b20/README.md?plain=1#L262

https://github.com/francoismichel/ssh3/blob/20f2894426742ee2fe92e33fa3d2521be7677b20/cmd/ssh3.go#L454

The SSH3 server has been started first with

ssh3-server --bind '[::]:8443' -cert ~/ssh3-cert.pem -key ~/ssh3-priv.key -generate-selfsigned-cert -v

to generate the key pair.

Subsequent invocations fail, when the -generate-selfsigned-cert flag is passed, why the final command line reads:

ssh3-server --bind '[::]:8443' -cert ~/ssh3-cert.pem -key ~/ssh3-priv.key -v

When spawning an associated SSH3 client with

ssh3 -use-oidc https://gitlab.example.com -v server:8443/ssh3-term

the OIDC provider, here GitLab, responds with:

An error has occurred

The redirect URI included is not valid.

This also happens when using the PKCE authentication scheme:

ssh3 -use-oidc https://gitlab.example.com -do-pkce -v server:8443/ssh3-term

Primary ways of mitigating this could be:

Additionally there may be a way to get rid of the client_secret on the client in case of only supporting PKCE authentication for non-confidential OIDC clients, e.g. by introducing a flat -require-pkce on the server.

References:

EthanHeilman commented 5 months ago

I was going to open a ticket but I see there is already an existing ticket.

The Google OP requires a static redirectURI and does not allow wildcards. Google does let you wildcard localhost ports. The redirect URI http://localhost/ will work for all ports.

Google OP configuration

This is the error I get when I use SSH3 oidc with google:

oidc google

What is the security goal?

Is the security goal of adding a random fragment to securely link the request to the OP with the response form the OP? If so OIDC already provides a more secure way of doing this using PKCE (RFC 7636: Proof Key for Code Exchange). The way it works is client creates:

$$CV \gets \{0,1\}^{256}$$

$$VC \gets \mbox{SHA256(CV)}$$

The client then adds VC to the auth request URI. Then to claim the ID Token associated with this request, the client must reveal VC to server. This means that even if an attacker intercepts the original request, the attacker only learns VC not CV. Additionally if the attacker triggers a redirectURI response which is not intended response from the client, when the client-supplies VC, it will break.

It looks like SSH3 supports PKCE, but the default in the SSH3 config is to for PKCE to be false.

doPKCE := flag.Bool("do-pkce", false, "if set perform PKCE challenge-response with oidc")

I don't see a reason why you would ever want PKCE to be false. Is there an OP that doesn't support PKCE?

Port diversity with known ports

In OpenPubkey we support choosing a port from a list of ports. This allows parties to add that list of ports to their redirectURI list if needed and also allows us to fail over if another application is already bound to that port. It might be worth copying this pattern to support OPs that don't wildcard localhost ports.

https://github.com/openpubkey/openpubkey/blob/024d958d6c26e0883c9a9e15cd37a123ce16fd7e/providers/google.go#L406-L414

septatrix commented 5 months ago

The random port is also problematic for firewalls

EthanHeilman commented 5 months ago

@almereyda If you want to play around SSH OIDC with a fix redirectURI, I created a PR #144 You can just pull from my branch and use it.

francoismichel commented 5 months ago

One reason for using that randomly-generated path was to avoid other programs to hijack the token by sending a fake request to the ssh3 waiting on localhost, causing it to close the socket, and then the attacker could listen on the same port and retrieve the token like that. It seems doable on pretty much any OS. But maybe am I missing something making such an attack undoable ?

EthanHeilman commented 5 months ago

@francoismichel If I understand your attack correctly, this was the attack that PKCE was introduced to defeat:

  1. The clients chooses initiating the auth request chooses a value CV and VC=Hash(CV) and initiates the auth request by sending VC to the OpenID Provider (OP),
  2. The user authenticates
  3. The OpenID Provider does the redirect to the redirect URI sending the auth code to the client via the localhost socket,
  4. The client sends the auth code and the CV value to the OpenID Provider which returns the ID Token.

An attacker who intercepts the auth code by listening on the localhost socket can not redeem that auth code for an ID Token because the attacker does not know a value CV such that VC=Hash(CV).

This is the value of enabling PKCE.


There is another known attack here which neither PKCE nor a randomized redirect URI does not fix.

  1. Attacker binds to a localhost port,
  2. Attacker opens browser to initiate auth request,
  3. User clicks I consent (in some IDPs that don't even need to consent if they have already consented for that client-id),
  4. Attacker gets auth code and redeems it for the ID Token

The user looking at a browser window that popped up has no easy way to determine that this was opened by the legitimate service running on localhost. There two mitigations:

Mobile device flows don't have this problem because app identity is known to the OS and so the iOS can enforce a map of redirect URIs to specific cryptographic identities associated with the candy crush app. There are some other solutions in this space, but using I favor the OpenPubkey cosigner approach.

In general it is very hard to protect secrets like this if an attacker has a programming process on your endpoint that can open ports. That can do so much bad stuff, that provided security against such a threat is diminishing returns.

septatrix commented 5 months ago

Why release the port at all? To be the same redirect URL and work best with reverse proxies and firewalls the port should be static anyhow. So I see no reason why the port should be release after using it?

EthanHeilman commented 5 months ago

@septatrix Releasing the port makes sense if you are using this as a cli where the cli isn't always running. The alternative is a daemon which starts at OS bootup and always holds that port or a set of ports, but that is a more complex client to build.

septatrix commented 5 months ago

@septatrix Releasing the port makes sense if you are using this as a cli where the cli isn't always running. The alternative is a daemon which starts at OS bootup and always holds that port or a set of ports, but that is a more complex client to build.

Ah I was under the assumption that the SSH3 server would be the one receiving the token. That seems like the intuitive solution

francoismichel commented 5 months ago

Thanks for the discussion. The attack with an attacker opening a browser window concurrently to SSH3 may not be easy to perform but it makes sense. I wish there was a generic way in OIDC to display some text to the user in the browser window so that it can check the browser window is indeed the right one. The difference in this scenario however, is that the attacker must either be root or be running with the same rights as the current user in order to be able to show a browser window in its desktop session, while the previous scenario (the one avoidable with PKCE), the attacker just needs to be able to listen on a port.

The reason why PKCE is disabled by default is simply because I couldn't make it to work by the time with that specific version of the oidc module and the google OP, but I may give it another try now. We should probably list the common OPs that support PKCE well and enable it by default.

francoismichel commented 5 months ago

Concerning the random URL, I am not strongly against removing it, but we should probably ensure that PKCE is enabled then, and use the random URL if not.

septatrix commented 5 months ago

I would also like to note that the RFC says to construct these loopback URLs using the numeric literals (127.0.0.1 and [::1] respectively) instead of "localhost".

Another option (which would also be necessary when one wants to implement a mobile client for SSH3) is to use private-use URI schemes like be.francoismichel.ssh3:/oauth2redirect/. This is also possible on desktop OS though it requires a more traditional installation. E.g. on linux a ssh3.desktop file would be required

EthanHeilman commented 3 months ago

@septatrix My two cents:

  1. Using numeric loopbacks seems reasonable. The only reason I like localhost is that is it generic to ipv4 and ipv6. It seems like it wouldn't be hard to just specify two redirect URIs one being the loopback for IPv4 and one being the loopback for IPv6.
  2. How well supported is native-app OAuth in OIDC on the desktop? I haven't build any systems using it and I don't know if there are any gotchas. I agree on mobile devices private-use schemes is both well supported and a security requirement. Are protocol handlers still used for this?
septatrix commented 3 months ago

@septatrix My two cents:

  1. Using numeric loopbacks seems reasonable. The only reason I like localhost is that is it generic to ipv4 and ipv6. It seems like it wouldn't be hard to just specify two redirect URIs one being the loopback for IPv4 and one being the loopback for IPv6.

In Linux binding to ::1 defaults to dual stack mode, i.e. it also received IPv4 packets

  1. How well supported is native-app OAuth in OIDC on the desktop? I haven't build any systems using it and I don't know if there are any gotchas. I agree on mobile devices private-use schemes is both well supported and a security requirement. Are protocol handlers still used for this?

Not really sure about this TBH