ACME client plugin
This adds support for a plugin that can be attached to a vhost to acquire and maintain its TLS cert automatically. It works the same with both OpenSSL and mbedTLS backends, but they can't share auth keys, delete the 'auth.jwk' file as it is in the example JSON when switching between libs
This commit is contained in:
parent
813b019bd1
commit
3ec7c1ab21
14 changed files with 2221 additions and 462 deletions
|
@ -4,7 +4,7 @@ env:
|
|||
global:
|
||||
- secure: "KhAdQ9ja+LBObWNQTYO7Df5J4DyOih6S+eerDMu8UPSO+CoWV2pWoQzbOfocjyOscGOwC+2PrrHDNZyGfqkCLDXg1BxynXPCFerHC1yc2IajvKpGXmAAygNIvp4KACDfGv/dkXrViqIzr/CdcNaU4vIMHSVb5xkeLi0W1dPnQOI="
|
||||
matrix:
|
||||
- LWS_METHOD=lwsws CMAKE_ARGS="-DLWS_WITH_LWSWS=ON -DLWS_WITH_HTTP2=1"
|
||||
- LWS_METHOD=lwsws CMAKE_ARGS="-DLWS_WITH_LWSWS=ON -DLWS_WITH_HTTP2=1 -DLWS_WITH_ACME=1"
|
||||
- LWS_METHOD=default
|
||||
- LWS_METHOD=noserver CMAKE_ARGS="-DLWS_WITHOUT_SERVER=ON"
|
||||
- LWS_METHOD=noclient CMAKE_ARGS="-DLWS_WITHOUT_CLIENT=ON"
|
||||
|
|
|
@ -27,6 +27,7 @@ option(LWS_WITH_PEER_LIMITS "Track peers and restrict resources a single peer ca
|
|||
option(LWS_WITH_ACCESS_LOG "Support generating Apache-compatible access logs" OFF)
|
||||
option(LWS_WITH_RANGES "Support http ranges (RFC7233)" ON)
|
||||
option(LWS_WITH_SERVER_STATUS "Support json + jscript server monitoring" OFF)
|
||||
option(LWS_WITH_ACME "Enable support for ACME automatic cert acquisition + maintenance (letsencrypt etc)" OFF)
|
||||
#
|
||||
# TLS library options... all except mbedTLS are basically OpenSSL variants.
|
||||
#
|
||||
|
@ -175,6 +176,12 @@ if (LWS_WITH_LWSWS)
|
|||
set(LWS_WITH_PEER_LIMITS 1)
|
||||
endif()
|
||||
|
||||
if (LWS_WITH_ACME)
|
||||
set (LWS_WITHOUT_CLIENT 0)
|
||||
set (LWS_WITHOUT_SERVER 0)
|
||||
set (LWS_WITH_JWS 1)
|
||||
endif()
|
||||
|
||||
if (LWS_WITH_JWS)
|
||||
set(LWS_WITH_LEJP 1)
|
||||
endif()
|
||||
|
@ -1711,6 +1718,11 @@ if (NOT LWS_WITHOUT_CLIENT)
|
|||
"plugins/protocol_client_loopback_test.c" "" "")
|
||||
endif(NOT LWS_WITHOUT_CLIENT)
|
||||
|
||||
if (LWS_WITH_ACME)
|
||||
create_plugin(protocol_lws_acme_client ""
|
||||
"plugins/acme-client/protocol_lws_acme_client.c" "" "")
|
||||
endif()
|
||||
|
||||
if (LWS_WITH_GENERIC_SESSIONS)
|
||||
create_plugin(protocol_generic_sessions ""
|
||||
"plugins/generic-sessions/protocol_generic_sessions.c"
|
||||
|
|
|
@ -79,6 +79,12 @@ on port 7681, non-SSL is provided. To set it up
|
|||
# sudo lwsws
|
||||
```
|
||||
|
||||
@section lwswsacme Using Letsencrypt or other ACME providers
|
||||
|
||||
Lws supports automatic provisioning and renewal of TLS certificates.
|
||||
|
||||
See ./READMEs/README.plugin-acme.md for examples of how to set it up on an lwsws vhost.
|
||||
|
||||
@section lwsogo Other Global Options
|
||||
|
||||
- `reject-service-keywords` allows you to return an HTTP error code and message of your choice
|
||||
|
@ -430,6 +436,17 @@ The file should be readable by lwsws, and for a little bit of extra security not
|
|||
have a file suffix, so lws would reject to serve it even if it could find it on
|
||||
a mount.
|
||||
|
||||
@section lwswscc Requiring a Client Cert on a vhost
|
||||
|
||||
You can make a vhost insist to get a client certificate from the peer before
|
||||
allowing the connection with
|
||||
|
||||
```
|
||||
"client-cert-required": "1"
|
||||
```
|
||||
|
||||
the connection will only proceed if the client certificate was signed by the
|
||||
same CA as the server has been told to trust.
|
||||
|
||||
@section lwswspl Lwsws Plugins
|
||||
|
||||
|
|
180
READMEs/README.plugin-acme.md
Normal file
180
READMEs/README.plugin-acme.md
Normal file
|
@ -0,0 +1,180 @@
|
|||
lws-acme-client Plugin
|
||||
======================
|
||||
|
||||
## Introduction
|
||||
|
||||
lws-acme-client is a protcol plugin for libwebsockets that implements an
|
||||
ACME client able to communicate with let's encrypt and other certificate
|
||||
providers.
|
||||
|
||||
It implements `tls-sni-01` challenge, and is able to provision tls certificates
|
||||
"from thin air" that are accepted by all the major browsers. It also manages
|
||||
re-requesting the certificate when it only has two weeks left to run.
|
||||
|
||||
It works with both the OpenSSL and mbedTLS backends.
|
||||
|
||||
## Overview for use
|
||||
|
||||
You need to:
|
||||
|
||||
- Provide name resolution to the IP with your server, ie, myserver.com needs to
|
||||
resolve to the IP that hosts your server
|
||||
|
||||
- Enable port forwarding / external firewall access to your port, usually 443
|
||||
|
||||
- Enable the "lws-acme-client" plugin on the vhosts you want it to manage
|
||||
certs for
|
||||
|
||||
- Add per-vhost options describing what should be in the certificate
|
||||
|
||||
After that the plugin will sort everything else out.
|
||||
|
||||
## Example lwsws setup
|
||||
|
||||
```
|
||||
"vhosts": [ {
|
||||
"name": "home.warmcat.com",
|
||||
"port": "443",
|
||||
"host-ssl-cert": "/etc/lwsws/acme/home.warmcat.com.crt.pem",
|
||||
"host-ssl-key": "/etc/lwsws/acme/home.warmcat.com.key.pem",
|
||||
"ignore-missing-cert": "1",
|
||||
"access-log": "/var/log/lwsws/test-access-log",
|
||||
"ws-protocols": [{
|
||||
"lws-acme-client": {
|
||||
"auth-path": "/etc/lwsws/acme/auth.jwk",
|
||||
"cert-path": "/etc/lwsws/acme/home.warmcat.com.crt.pem",
|
||||
"key-path": "/etc/lwsws/acme/home.warmcat.com.key.pem",
|
||||
"directory-url": "https://acme-staging.api.letsencrypt.org/directory",
|
||||
"country": "TW",
|
||||
"state": "Taipei",
|
||||
"locality": "Xiaobitan",
|
||||
"organization": "Crash Barrier Ltd",
|
||||
"common-name": "home.warmcat.com",
|
||||
"email": "andy@warmcat.com"
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Required PVOs
|
||||
|
||||
Notice that the `"host-ssl-cert"` and `"host-ssl-key"` entries have the same
|
||||
meaning as usual, they point to your certificate and private key. However
|
||||
because the ACME plugin can provision these, you should also mark the vhost with
|
||||
`"ignore-missing-cert" : "1"`, so lwsws will ignore what will initially be
|
||||
missing certificate / keys on that vhost, and will set about creating the
|
||||
necessary certs and keys instead of erroring out.
|
||||
|
||||
You must make sure the directories mentioned here exist, lws doesn't create them
|
||||
for you. They should be 0700 root:root, even if you drop lws privileges.
|
||||
|
||||
If you are implementing support in code, this corresponds to making sure the
|
||||
vhost creating `info.options` has the `LWS_SERVER_OPTION_IGNORE_MISSING_CERT`
|
||||
bit set.
|
||||
|
||||
Similarly, in code, the each of the per-vhost options shown above can be
|
||||
provided in a linked-list of structs at vhost creation time. See
|
||||
`./test-apps/test-server-v2.0.c` for example code for providing pvos.
|
||||
|
||||
### auth-path
|
||||
|
||||
This is where the plugin will store the auth keys it generated.
|
||||
|
||||
### cert-path
|
||||
|
||||
Where the plugin will store the certificate file. Should match `host-ssl-cert`
|
||||
that the vhost wants to use.
|
||||
|
||||
The path should include at least one 0700 root:root directory.
|
||||
|
||||
### key-path
|
||||
|
||||
Where the plugin will store the certificate keys. Again it should match
|
||||
`host-ssl-key` the vhost is trying to use.
|
||||
|
||||
The path should include at least one 0700 root:root directory.
|
||||
|
||||
### directory-url
|
||||
|
||||
This defines the URL of the certification server you will get your
|
||||
certificates from. For let's encrypt, they have a "practice" one
|
||||
|
||||
- `https://acme-staging.api.letsencrypt.org/directory`
|
||||
|
||||
and they have a "real" one
|
||||
|
||||
- `https://acme-v01.api.letsencrypt.org/directory`
|
||||
|
||||
the main difference is the CA certificate for the real one is in most browsers
|
||||
already, but the staging one's CA certificate isn't. The staging server will
|
||||
also let you abuse it more in terms of repeated testing etc.
|
||||
|
||||
It's recommended you confirm expected operation with the staging directory-url,
|
||||
and then switch to the "real" URL.
|
||||
|
||||
### common-name
|
||||
|
||||
Your server DNS name, like "libwebsockets.org". The remote ACME server will
|
||||
use this to find your server to perform the SNI challenges.
|
||||
|
||||
### email
|
||||
|
||||
The contact email address for the certificate.
|
||||
|
||||
## Optional PVOs
|
||||
|
||||
These are not included in the cert by letsencrypt
|
||||
|
||||
### country
|
||||
|
||||
Two-letter country code for the certificate
|
||||
|
||||
### state
|
||||
|
||||
State "or province" for the certificate
|
||||
|
||||
### locality
|
||||
|
||||
Locality for the certificate
|
||||
|
||||
### organization
|
||||
|
||||
Your company name
|
||||
|
||||
## Security / Key storage considerations
|
||||
|
||||
The `lws-acme-client` plugin is able to provision and update your certificate
|
||||
and keys in an entirely root-only storage environment, even though lws runs
|
||||
as a different uid / gid with no privileges to access the storage dir.
|
||||
|
||||
It does this by opening and holding two WRONLY fds on "update paths" inside the
|
||||
root directory structure for each cert and key it manages; these are the normal
|
||||
cert and key paths with `.upd` appended. If during the time the server is up
|
||||
the certs become within two weeks of expiry, the `lws-acme-client` plugin will
|
||||
negotiate new certs and write them to the file descriptors.
|
||||
|
||||
Next time the server starts, if it sees `.upd` cert and keys, it will back up
|
||||
the old ones and copy them into place as the new ones, before dropping privs.
|
||||
|
||||
To also handle the long-uptime server case, lws will update the vhost with the
|
||||
new certs using in-memory temporary copies of the cert and key after updating
|
||||
the cert.
|
||||
|
||||
In this way the cert and key live in root-only storage but the vhost is kept up
|
||||
to date dynamically with any cert changes as well.
|
||||
|
||||
## Multiple vhosts using same cert
|
||||
|
||||
In the case you have multiple vhosts using of the same cert, just attach
|
||||
the `lws-acme-client` plugin to one instance. When the cert updates, all the
|
||||
vhosts are informed and vhosts using the same filepath to access the cert will
|
||||
be able to update their cert.
|
||||
|
||||
## Implementation point
|
||||
|
||||
You will need to remove the auth keys when switching from OpenSSL to
|
||||
mbedTLS. They will be regenerated automatically. It's the file at this
|
||||
path:
|
||||
|
||||
```
|
||||
"auth-path": "/etc/lwsws/acme/auth.jwk",
|
||||
```
|
|
@ -98,6 +98,7 @@ STORE_IN_ROM static const char * const set[] = {
|
|||
"connect ",
|
||||
"head ",
|
||||
"te:", /* http/2 wants it to reject it */
|
||||
"replay-nonce:", /* ACME */
|
||||
|
||||
"", /* not matchable */
|
||||
|
||||
|
|
930
lib/lextable.h
930
lib/lextable.h
File diff suppressed because it is too large
Load diff
|
@ -3858,6 +3858,8 @@ enum lws_token_indexes {
|
|||
WSI_TOKEN_CONNECT = 81,
|
||||
WSI_TOKEN_HEAD_URI = 82,
|
||||
WSI_TOKEN_TE = 83,
|
||||
WSI_TOKEN_REPLAY_NONCE = 84,
|
||||
|
||||
/****** add new things just above ---^ ******/
|
||||
|
||||
/* use token storage to stash these internally, not for
|
||||
|
@ -5583,7 +5585,13 @@ enum {
|
|||
LWS_TLS_REQ_ELEMENT_COMMON_NAME,
|
||||
LWS_TLS_REQ_ELEMENT_EMAIL,
|
||||
|
||||
LWS_TLS_REQ_ELEMENT_COUNT
|
||||
LWS_TLS_REQ_ELEMENT_COUNT,
|
||||
LWS_TLS_SET_DIR_URL = LWS_TLS_REQ_ELEMENT_COUNT,
|
||||
LWS_TLS_SET_AUTH_PATH,
|
||||
LWS_TLS_SET_CERT_PATH,
|
||||
LWS_TLS_SET_KEY_PATH,
|
||||
|
||||
LWS_TLS_TOTAL_COUNT
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1773,3 +1773,26 @@ uint16_t lws_esp32_sine_interp(int n)
|
|||
sine_lu((n >> 4) + 1) * (n & 15)) / 15;
|
||||
}
|
||||
|
||||
/* we write vhostname.cert.pem and vhostname.key.pem, 0 return means OK */
|
||||
|
||||
int
|
||||
lws_plat_write_cert(struct lws_vhost *vhost, int is_key, int fd, void *buf,
|
||||
int len)
|
||||
{
|
||||
char name[64];
|
||||
int n;
|
||||
|
||||
lws_snprintf(name, sizeof(name) - 1, "%s-%s.pem", vhost->name,
|
||||
is_key ? "key" : "cert");
|
||||
|
||||
if (nvs_open("lws-station", NVS_READWRITE, &nvh))
|
||||
return 1;
|
||||
|
||||
n = nvs_set_blob(nvh, ssl_names[n], pss->buffer, pss->file_length);
|
||||
if (n)
|
||||
nvs_commit(nvh);
|
||||
|
||||
nvs_close(nvh);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
|
|
@ -706,3 +706,11 @@ lws_plat_init(struct lws_context *context,
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
lws_plat_write_cert(struct lws_vhost *vhost, int is_key, int fd, void *buf,
|
||||
int len)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -319,3 +319,10 @@ lws_plat_init(struct lws_context *context,
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
lws_plat_write_cert(struct lws_vhost *vhost, int is_key, int fd, void *buf,
|
||||
int len)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -863,3 +863,17 @@ lws_plat_init(struct lws_context *context,
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
lws_plat_write_cert(struct lws_vhost *vhost, int is_key, int fd, void *buf,
|
||||
int len)
|
||||
{
|
||||
int n;
|
||||
|
||||
n = write(fd, buf, len);
|
||||
|
||||
fsync(fd);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
|
||||
return n != len;
|
||||
}
|
||||
|
|
|
@ -749,3 +749,15 @@ int fork(void)
|
|||
exit(0);
|
||||
}
|
||||
|
||||
int
|
||||
lws_plat_write_cert(struct lws_vhost *vhost, int is_key, int fd, void *buf,
|
||||
int len)
|
||||
{
|
||||
int n;
|
||||
|
||||
n = write(fd, buf, len);
|
||||
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
|
||||
return n != len;
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ lws_mbedtls_sni_cb(void *arg, mbedtls_ssl_context *mbedtls_ctx,
|
|||
return 0;
|
||||
}
|
||||
|
||||
lwsl_notice("SNI: Found: %s:%d\n", servername, vh->listen_port);
|
||||
lwsl_info("SNI: Found: %s:%d\n", servername, vh->listen_port);
|
||||
|
||||
/* select the ssl ctx from the selected vhost for this conn */
|
||||
SSL_set_SSL_CTX(ssl, vhost->ssl_ctx);
|
||||
|
|
1465
plugins/acme-client/protocol_lws_acme_client.c
Normal file
1465
plugins/acme-client/protocol_lws_acme_client.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue