WIP: fix http-01 challenges

This commit is contained in:
AJ ONeal 2022-06-05 03:41:00 -06:00
parent 79231a6de8
commit 975e6bab3e
14 changed files with 169 additions and 33 deletions

10
bin/build-mgmt.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
go generate -mod vendor ./...
go build -mod vendor -race -o ./telebit-mgmt cmd/mgmt/*.go
my_version="$(./telebit-mgmt version | cut -d' ' -f4 | sd ':' '.' | sd 'T' '_')"
rm -rf ~/srv/telebit-mgmt/bin/telebit-mgmt
rsync -avhP telebit-mgmt ~/srv/telebit-mgmt/bin/"telebit-mgmt-${my_version}"
ln -s ~/srv/telebit-mgmt/bin/"telebit-mgmt-${my_version}" ~/srv/telebit-mgmt/bin/telebit-mgmt

11
bin/build-relay.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
go generate -mod vendor ./...
go build -mod vendor -race -o ./telebit-relay cmd/telebit/*.go
my_version="$(./telebit-relay version | cut -d' ' -f4 | sd ':' '.' | sd 'T' '_')"
rm -rf ~/srv/telebit-relay/bin/telebit-relay
rsync -avhP telebit-relay ~/srv/telebit-relay/bin/"telebit-relay-${my_version}"
ln -s ~/srv/telebit-relay/bin/"telebit-relay-${my_version}" ~/srv/telebit-relay/bin/telebit-relay
sudo setcap 'cap_net_bind_service=+ep' ~/srv/telebit-relay/bin/"telebit-relay-${my_version}"

1
bin/deploy-postgres.sh Normal file
View File

@ -0,0 +1 @@
webi postgres

6
bin/psql-connect.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
set -u
source .env
psql "${DB_URL}"

View File

@ -47,6 +47,19 @@ See [`examples/relay.env`][relay-env] for detail explanations.
Note: It is not necessary to specify the `--flags` when using the ENVs.
## API
### Discovery
Each telebit relay with expose its discovery endpoint at
- `.well-known/telebit.app/index.json`
The response will look something like
```json
```
## System Services
You can use `serviceman` to run `postgres`, `telebit`, and `telebit-mgmt` as system services

View File

@ -314,7 +314,7 @@ func parseFlagsAndENVs() {
flag.BoolVar(&config.enableHTTP01, "acme-http-01", false, "enable HTTP-01 ACME challenges")
flag.BoolVar(&config.enableTLSALPN01, "acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
flag.StringVar(&config.logPath, "outfile", "", "where to direct output (default system logger or OS stdout)")
flag.StringVar(&config.acmeRelay, "acme-relay-url", "", "the base url of the ACME relay, if different from relay's directives")
flag.StringVar(&config.acmeRelay, "acme-relay-url", "", "the base url of the ACME relay, if different from telebit relay's directives")
flag.StringVar(&config.acmeDNS01Relay, "acme-dns-01-relay-url", "", "the base url of the ACME DNS-01 relay, if different from ACME relay")
flag.StringVar(&config.acmeHTTP01Relay, "acme-http-01-relay-url", "", "the base url of the ACME HTTP-01 relay, if different from ACME relay")
flag.StringVar(&config.authURL, "auth-url", "", "the base url for authentication, if not the same as the tunnel relay")
@ -592,10 +592,15 @@ func parseFlagsAndENVs() {
}
func tokener() string {
secret := config.pairwiseSecret
if 0 == len(config.tunnelRelay) {
secret = ClientSecret
}
token := config.token
if 0 == len(token) {
var err error
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
token, err = authstore.HMACToken(secret, config.leeway)
if dbg.Debug {
fmt.Printf("[debug] app_id: %q\n", VendorID)
//fmt.Printf("[debug] client_secret: %q\n", ClientSecret)

View File

@ -4,13 +4,15 @@ set -e
set -u
source .env
MGMT_URL="${MGMT_URL:-"http://localhost:3000/api"}"
MGMT_PORT="${MGMT_PORT:-3000}"
MGMT_URL="${MGMT_URL:-"http://localhost:${MGMT_PORT}/api"}"
TOKEN=$(go run cmd/signjwt/*.go \
--expires-in 1m \
--vendor-id "$VENDOR_ID" \
--secret "$RELAY_SECRET" \
--machine-ppid "$RELAY_SECRET"
TOKEN=$(
go run cmd/signjwt/*.go \
--expires-in 1m \
--vendor-id "$VENDOR_ID" \
--secret "$RELAY_SECRET" \
--machine-ppid "$RELAY_SECRET"
)
echo "MGMT URL: $MGMT_URL"

View File

@ -9,6 +9,7 @@ TUNNEL_DOMAIN=tunnel.example.com
# For mgmt server itself
SECRET=XxxxxxxxxxxxxxxX
VENDOR_ID='example.com'
DB_URL=postgres://postgres:postgres@localhost:5432/postgres
# PORT

View File

@ -4,6 +4,7 @@
# This should be the same as the MGMT server secret
# It is used for JWT token creation and verification
SECRET=xxxxxxxxxxxxxxxx
VENDOR_ID='example.com'
# VERBOSE=true
# This will cause more verbose logs

View File

@ -2,20 +2,21 @@
source .env
TOKEN=$(go run cmd/signjwt/*.go \
--expires-in 1m \
--vendor-id "$VENDOR_ID" \
--secret "$MGMT_SECRET" \
--machine-ppid "$MGMT_SECRET"
TOKEN=$(
go run cmd/signjwt/*.go \
--expires-in 1m \
--vendor-id "$VENDOR_ID" \
--secret "$MGMT_SECRET" \
--machine-ppid "$MGMT_SECRET"
)
echo "MGMT_TOKEN: $TOKEN"
my_parts=$(
go run cmd/signjwt/*.go \
--vendor-id "$VENDOR_ID" \
--secret "$MGMT_SECRET" \
--machine-ppid "$MGMT_SECRET" \
--machine-ppid-only
go run cmd/signjwt/*.go \
--vendor-id "$VENDOR_ID" \
--secret "$MGMT_SECRET" \
--machine-ppid "$MGMT_SECRET" \
--machine-ppid-only
)
my_ppid=$(echo $my_parts | cut -d' ' -f1)
my_keyid=$(echo $my_parts | cut -d' ' -f2)

View File

@ -0,0 +1,54 @@
package http01fs
import (
"context"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/mholt/acmez/acme"
)
const (
challengeDir = ".well-known/acme-challenge"
tmpBase = "acme-tmp"
)
var Provider Solver
// Solver implements the challenge.Provider interface.
type Solver struct {
Path string
}
// Present creates a HTTP-01 Challenge Token
func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error {
log.Println("Present HTTP-01 challenge solution for", ch.Identifier.Value)
myBase := s.Path
if 0 == len(tmpBase) {
myBase = "acme-tmp"
}
challengeBase := filepath.Join(myBase, ch.Identifier.Value, ".well-known/acme-challenge")
_ = os.MkdirAll(challengeBase, 0700)
tokenPath := filepath.Join(challengeBase, ch.Token)
return ioutil.WriteFile(tokenPath, []byte(ch.KeyAuthorization), 0600)
}
// CleanUp deletes an HTTP-01 Challenge Token
func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error {
log.Println("CleanUp HTTP-01 challenge solution for", ch.Identifier.Value)
myBase := s.Path
if 0 == len(tmpBase) {
myBase = "acme-tmp"
}
// always try to remove, as there's no harm
tokenPath := filepath.Join(myBase, ch.Identifier.Value, challengeDir, ch.Token)
_ = os.Remove(tokenPath)
return nil
}

View File

@ -1,17 +1,18 @@
package mgmt
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"git.rootprojects.org/root/telebit/internal/http01fs"
"github.com/go-acme/lego/v3/challenge"
"github.com/go-chi/chi"
"github.com/mholt/acmez/acme"
)
const (
@ -65,12 +66,20 @@ func handleDNSRoutes(r chi.Router) {
r.Route("/http", handleACMEChallenges)
}
func isSlugAllowed(domain, slug string) bool {
if "*" == slug {
return true
}
// ex: "abc.devices.example.com" has prefix "abc."
return strings.HasPrefix(domain, slug+".")
}
func createChallenge(w http.ResponseWriter, r *http.Request) {
domain := chi.URLParam(r, "domain")
ctx := r.Context()
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
if !ok || !strings.HasPrefix(domain+".", claims.Slug) {
if !ok || !isSlugAllowed(domain, claims.Slug) {
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
return
@ -94,10 +103,15 @@ func createChallenge(w http.ResponseWriter, r *http.Request) {
if "" == ch.Token || "" == ch.KeyAuth {
err = errors.New("missing token and/or key auth")
} else if strings.Contains(ch.Type, "http") {
challengeBase := filepath.Join(tmpBase, ch.Domain, ".well-known/acme-challenge")
_ = os.MkdirAll(challengeBase, 0700)
tokenPath := filepath.Join(challengeBase, ch.Token)
err = ioutil.WriteFile(tokenPath, []byte(ch.KeyAuth), 0600)
provider := &http01fs.Provider
provider.Present(context.Background(), acme.Challenge{
Token: ch.Token,
KeyAuthorization: ch.KeyAuth,
Identifier: acme.Identifier{
Value: ch.Domain,
Type: "dns", // TODO is this correct??
},
})
} else {
// TODO some additional error checking before the handoff
//ch.error = make(chan error, 1)
@ -120,6 +134,7 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) {
// TODO authenticate
ch := Challenge{
Type: chi.URLParam(r, "challengeType"),
Domain: chi.URLParam(r, "domain"),
Token: chi.URLParam(r, "token"),
KeyAuth: chi.URLParam(r, "keyAuth"),
@ -131,10 +146,17 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) {
if "" == ch.Token || "" == ch.KeyAuth {
err = errors.New("missing token and/or key auth")
} else if strings.Contains(ch.Type, "http") {
// always try to remove, as there's no harm
tokenPath := filepath.Join(tmpBase, ch.Domain, challengeDir, ch.Token)
_ = os.Remove(tokenPath)
provider := &http01fs.Provider
provider.CleanUp(context.Background(), acme.Challenge{
Token: ch.Token,
KeyAuthorization: ch.KeyAuth,
Identifier: acme.Identifier{
Value: ch.Domain,
Type: "dns", // TODO is this correct??
},
})
} else {
// TODO what if DNS-01 is not enabled?
cleanups <- &ch
err = <-ch.error
}

View File

@ -83,7 +83,7 @@ func (s *PGStore) SetMaster(secret string) error {
machine_ppid=$1,
shared_key=$1,
public_key=$2,
deleted_at='1970-01-01 00:00:00'
deleted_at='1970-01-01 00:00:00'
WHERE slug = '*'
`
_, err = s.dbx.ExecContext(ctx, query, auth.MachinePPID, auth.PublicKey)
@ -223,6 +223,7 @@ func (s *PGStore) Get(id string) (*Authorization, error) {
`
// if the id is actually the secret, we want the public form
// (we do this to protect against a timing attack)
kid := id
pubby := ToPublicKeyString(id)
if len(id) > 24 {
id = id[:24]

View File

@ -69,8 +69,12 @@ func RouteAll(r chi.Router) {
// TODO make parallel?
// TODO make cancellable?
ch := <-presenters
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
ch.error <- err
if nil != provider {
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
ch.error <- err
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for present")
}
}
}()
@ -79,7 +83,11 @@ func RouteAll(r chi.Router) {
// TODO make parallel?
// TODO make cancellable?
ch := <-cleanups
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
if nil != provider {
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
}
}
}()