crossbario / autobahn-python

WebSocket and WAMP in Python for Twisted and asyncio
https://crossbar.io/autobahn
MIT License
2.47k stars 768 forks source link

asyncio support & docs for using TLS client certificates #761

Open petri opened 7 years ago

petri commented 7 years ago

Autobahn Python can use TLS client certificates to authenticate for example with crossbar, when using Twisted. However, there is no documentation anywhere on what's the status on asyncio. The official word appears to be "it might work". Hence this issue.

FWIW, I tried the following client code:

ssl_c = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_c.load_cert_chain(certfile='client.crt', keyfile='client.key')
ssl_c.load_verify_locations(cafile='xxx.com.cert')

runner = ApplicationRunner(
    u"wss://xxx.com:443/ws", u"realm1", ssl=ssl_c, extra={"authid": "myauthid"}
)

The resulting client error:

wamp.error.no_auth_method: cannot authenticate using any of the offered authmethods ['anonymous']

Crossbar log error:

crossbar.router.session.RouterSession] client requested valid, but unconfigured authentication method anonymous

As far as I can tell, crossbar config for server & client TLS use is ok per all the docs, when these errors occur. I can post the config here; I posted it to the crossbar google group as well, already.

meejah commented 7 years ago

Yes, please post the config.

Ultimate having docs and (ideally) an example for this would be great!

petri commented 7 years ago

Here's the crossbar config used, with the domain & some other stuff obfuscated.

  version: 2
  controller: {}
  workers:
  - type: router
    realms:
    - name: 'realm1'
      roles:
      - name: abc
        permissions:
        - uri: ''
          match: prefix
          allow:
            call: true
            register: true
            publish: true
            subscribe: true
          disclose:
            caller: false
            publisher: false
          cache: true
    transports:
      - type: websocket
        auth:
          tls:
            type: static
            principals:
              client0:
                certificate-sha1: "5C:CE:DE:00:51:0F:5D:9F:93:45:3E:92:CE:11:FD:17:13:AA:DB:13"
                role: abc
        endpoint:
          type: tcp
          port: 443
          tls:
            key: /etc/letsencrypt/live/xxx.com/privkey.pem
            certificate: /etc/letsencrypt/live/xxx.com/cert.pem
            chain_certificates: ["/etc/letsencrypt/live/xxx.com/chain.pem"]
            ca_certificates: ["clientca.cert.pem"]
        url: wss://xxx.com/ws
      - type: web
        endpoint:
          type: tcp
          port: 8080
          tls:
            key: /etc/letsencrypt/live/xxx.com/privkey.pem
            certificate: /etc/letsencrypt/live/xxx.com/cert.pem
            chain_certificates: ["/etc/letsencrypt/live/xxx.com/chain.pem"]
        paths:
          /:
            type: static
            directory: ../www
petri commented 7 years ago

I tried connecting using curl, and got no error up to the point of websocket upgrade (whereby failure is expected):

curl --cacert ./clientca.cert.pem --cert ./client.crt --key ./client.key https://xxx.com:443/ws

Likewise, dropping the cert and key from curl command results in SSL error in crossbar logs. So it looks like the server might be configured properly...

petri commented 7 years ago

Taking a look at the crossbar logs, it seems to me the abovementioned "cannot authenticate" error happens after the TLS authentication and connection already succeeded... how weird is that? It's as if the WAMP level does not understand the authentication already took place?

petri commented 7 years ago

Bummer. TLS client certificate auth does work. It's joining the realm that fails with the abovementioned error. Should've guessed, given the error is in WAMP code :(

petri commented 7 years ago

Solved. Apparently ApplicationSession.join() requires an extra param in this case:

def join(self, realm, authmethods=None, authid=None, authrole=None, authextra=None):

So, all that's required for the TLS client cert auth to work with the above crossbar configuration is passing authmethods=['tls'].

This requirement should be explicitly mentioned in the docs and examples. Maybe it's already there in that one example I did not thoroughly peruse through :(

Even better - it would alleviate the complexity of configuring all this if the ApplicationSession base class join default implementation could check the SSLContext for the presence of TLS client certificate and inject the tls authmethod upon join, if it's not explicitly given by the user. How's that as an idea?

oberstet commented 7 years ago

@petri so you got everything working as you expect?

Sorry for costing you time figuring this out .. we definitely should document that and probably also should add more asyncio examples.

Even better - it would alleviate the complexity of configuring all this if the ApplicationSession ... SNIP

I'm not sure tbh. It adds magic which might come unexpected. And if we had documented it / had an example, I'd say it is trivial to use (well, at least trivial to activate): authmethods=['tls']. This also serves as documentation: user wants to use TLS client auth. Otherwise, it is implicit.

petri commented 7 years ago

I understand. But it's already explicit, is it not, given that the user has configured the SSL context for the client certificate auth?

But perhaps this is really a question of whether or not the join authmethods should only refer to WAMP-level realm authentication methods, overall?

meejah commented 7 years ago

Yeah, it's kind of a fine line/point. Personally, I think "being explicit" is nice -- especially if we fix the docs so it doesn't take digging to figure that out. BUT on the other hand if you already leaped through enough fire-y hoops to make a client-side certificate work, ...

That said, I'm trying to imagine a case where you'd want to do TLS client-auth on the transport, but do some other thing to authenticate the realm at the WAMP level. If there's never such a case, then maybe "non-explict, slightly magic" is fine...

I do have some PoC stuff in the works to make authentication way better overall -- so you don't have to muck around with join() or overriding onChallenge at all (except for "odd" cases).

petri commented 7 years ago

I guess all WAMP clients of a customer could share a client certificate, and then authenticate using something else, would that be a possibility? I think it might.

Somehow, configuring lower-level auth at WAMP level just feels it goes completely against the whole idea of separation of networking layers.

Another thought: what if we configure TLS client certificates for a "plain" websocket connection? Then the SSL context is all that it takes?

meejah commented 7 years ago

Authenticating to WAMP realms via TLS is definitely kind of the "odd one out" as far as crossing the boundaries between transport and protocol -- it's kind of the "higher" protocol layer delegating auth to the "lower" transport layer. (Or, sort of saying "if-and-only-if the transport is client-authenticated, you get a free pass to the realm X").

So, that's kind of a point in favour of "explicit" -- you're saying, "I trust the TLS auth enough that I don't care about any other authentication for this realm". Whereas, I can certainly see that some people might regard TLS as "bare minimum" (even with client-side certs) to get in the door -- and still want higher-level auth.

(BTW, the "auth stuff" I mention two comments up is more-general than TLS etc...sorry for distracting this ticket :/ )

oberstet commented 7 years ago

@petri Separating layers is certainly not out of fashion;) Separation of concerns and decoupling are top design goals of WAMP. Eg witness WAMPs ability to run over different transports (WebSocket|RawSocket|Longpoll X TCP|TLS|UDS X JSON|MsgPack|UBJSON|CBOR = dozens of transports) transparently (that is, with no app code change at all).

I can see why TLS client auth could be seen as blurring the line. In a way it does. On the other hand, WAMP always authenticates at the WAMP level (during the WAMP HELLO, CHALLENGE, AUTHENTICATE, WELCOME message exchange). But WAMP allows to derive credentials from the underlying transport. Eg with WebSocket, which starts as HTTP, you can use HTTP cookies. You can't use cookies with RawSocket. Similar with TLS. But it is always the WAMP layer that does final auth decision (eg Dynamic-TLS .. the dynamic authenticator gets the cert info, and will decide. It is not the TLS layer that decides, for example, here).

Then, using authmethods=["tls", "cookie", "wampcra"] will behave differently than authmethods=["cookie", "tls", "wampcra"] etc (a router will chose the first authmethod it is willing to perform for the given client - router decides, client announces preference).

Then, we want to add multi-factor auth to WAMP. Eg this could look like authmethods=[["tls", "wampcra"], ["tls", "cookie"]] - either require TLS client cert and WAMP-CRA or TLS client cert and Cookie.

You cannot currently do that - and this is a real gap. We definitely want multi-factor auth ..

I'd also like to point out WAMP-cryptosign - for our own use in WAMP/Crossbar.io based apps, we'll be moving to WAMP-cryptosign (as soon as I find time to add it to AutobahnJS;).

Finally, I agree with @meejah : a) explicit is better than implicit and b) the real issue rgd AutobahnPython API is in ApplicationRunner. It is too rigid, not flexible etc .. it works "ok", but I am not happy with it. meejah has been working on different flavors of "new API", and I hope we get this nailed and shipped in a not too distant future.

petri commented 7 years ago

Yes. And I agree that explicit is better - I just feel it already is. But like @meejah said, it's a fine line. Thanks for laying out the big picture.