glyph / txsni

Simple support for running a TLS server with Twisted.
MIT License
25 stars 10 forks source link

send different certificate if acme-tls/1 negotated with alpn #26

Closed dholth closed 5 years ago

dholth commented 5 years ago

As you probably know letsencrypt requires alpn negotiation instead of just SNI these days.

This change intercepts alpn negotiation so that acme-tls/1 is always used if the client supports it. Then txsni chooses certificates from a second mapping, currently just an alpn/ directory underneath the default mapping, to use for that connection. When letsencrypt sees that special cert it knows you control the domain.

So if you have already generated the special certificate and placed it in acme/hostname.pem, txsni will be able to make letsencrypt happy. Then your ACME client can finish issuing you a new cert.

I expect to use this with dehydrated, the shell script acme client, and possibly with a new mapping object that can do dehydrated's separate key / cert layout. There's not enough here for txsni's "even the first request succeeds after waiting for the new certificate" but it should be really handy for development.

dholth commented 5 years ago

fixes #25

codecov-io commented 5 years ago

Codecov Report

Merging #26 into master will decrease coverage by 0.79%. The diff coverage is 80%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master     #26     +/-   ##
========================================
- Coverage      95%   94.2%   -0.8%     
========================================
  Files           6       6             
  Lines         400     414     +14     
  Branches       28      30      +2     
========================================
+ Hits          380     390     +10     
- Misses         12      15      +3     
- Partials        8       9      +1
Impacted Files Coverage Δ
txsni/parser.py 100% <100%> (ø) :arrow_up:
txsni/snimap.py 86.6% <76.47%> (-2.4%) :arrow_down:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 5014c14...c9650a7. Read the comment docs.

dholth commented 5 years ago

It's unfortunate that we have to choose a certificate for our hostname before we can immediately switch to the acme one

dholth commented 5 years ago

How dehydrated stores its certificates. letsencrypt doesn't agree with us yet, saying "detail": "remote error: tls: no application protocol"

import pem.twisted

class DehydratedMap(object):
    """
    Dehydrated's certs, in per-hostname subdirectories
    """
    def __init__(self, directoryPath):
        self.directoryPath = directoryPath

    def __getitem__(self, hostname):
        if hostname is None:
            hostname = b"DEFAULT"
        hostPath = self.directoryPath.child(hostname)
        keyPath = hostPath.child('privkey.pem')
        fullchainPath = hostPath.child('fullchain.pem')
        print('regular', hostname, keyPath, fullchainPath)
        if keyPath.isfile() and fullchainPath.isfile():
            print('regular', hostname, 'found')
            return pem.twisted.certificateOptionsFromFiles(keyPath.path, fullchainPath.path)
        else:
            raise KeyError("no pem file for " + hostname.decode('latin1'))

class DehydratedAcmeMap(object):
    """
    Dehydrated's ALPN certs, in files
    """
    def __init__(self, directoryPath):
        self.directoryPath = directoryPath

    def __getitem__(self, hostname):
        print('acme', hostname)
        certPath = self.directoryPath.child(hostname).siblingExtension(".crt.pem")
        keyPath = self.directoryPath.child(hostname).siblingExtension(".key.pem")
        if certPath.isfile() and keyPath.isfile():
            print('acme', hostname, 'found')
            return pem.twisted.certificateOptionsFromFiles(keyPath.path, certPath.path)
        else:
            raise KeyError("no alpn cert for " + hostname.decode('latin1'))
dholth commented 5 years ago

It at least works if the alpn certificate is the only one. SNIMap(acme_mapping)

dholth commented 5 years ago

I've cracked it. We already use bio so Python sees all the bytes, and we already proxy Connection. Trap the first call to bio to search for alpn in the first packet, if True load certificates from a different directory during SNI. Cheesy!

dholth commented 5 years ago

will replace

glyph commented 5 years ago

@dholth What replaced this?

dholth commented 5 years ago

https://github.com/glyph/txsni/pull/28

On Sat, Aug 3, 2019, at 6:24 PM, Glyph wrote:

@dholth https://github.com/dholth What replaced this?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/glyph/txsni/pull/26?email_source=notifications&email_token=AABSZEUUI5QEXQU6AYZEWJDQCYAQFA5CNFSM4HASGI52YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD3PWXOA#issuecomment-517958584, or mute the thread https://github.com/notifications/unsubscribe-auth/AABSZEWWCSQVVW5Q4OY5ELTQCYAQFANCNFSM4HASGI5Q.

glyph commented 5 years ago

Thanks.