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. 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 ## System Services
You can use `serviceman` to run `postgres`, `telebit`, and `telebit-mgmt` as 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.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.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.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.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.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") 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 { func tokener() string {
secret := config.pairwiseSecret
if 0 == len(config.tunnelRelay) {
secret = ClientSecret
}
token := config.token token := config.token
if 0 == len(token) { if 0 == len(token) {
var err error var err error
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway) token, err = authstore.HMACToken(secret, config.leeway)
if dbg.Debug { if dbg.Debug {
fmt.Printf("[debug] app_id: %q\n", VendorID) fmt.Printf("[debug] app_id: %q\n", VendorID)
//fmt.Printf("[debug] client_secret: %q\n", ClientSecret) //fmt.Printf("[debug] client_secret: %q\n", ClientSecret)

View File

@ -4,9 +4,11 @@ set -e
set -u set -u
source .env 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 \ TOKEN=$(
go run cmd/signjwt/*.go \
--expires-in 1m \ --expires-in 1m \
--vendor-id "$VENDOR_ID" \ --vendor-id "$VENDOR_ID" \
--secret "$RELAY_SECRET" \ --secret "$RELAY_SECRET" \

View File

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

View File

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

View File

@ -2,7 +2,8 @@
source .env source .env
TOKEN=$(go run cmd/signjwt/*.go \ TOKEN=$(
go run cmd/signjwt/*.go \
--expires-in 1m \ --expires-in 1m \
--vendor-id "$VENDOR_ID" \ --vendor-id "$VENDOR_ID" \
--secret "$MGMT_SECRET" \ --secret "$MGMT_SECRET" \

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

View File

@ -223,6 +223,7 @@ func (s *PGStore) Get(id string) (*Authorization, error) {
` `
// if the id is actually the secret, we want the public form // if the id is actually the secret, we want the public form
// (we do this to protect against a timing attack) // (we do this to protect against a timing attack)
kid := id
pubby := ToPublicKeyString(id) pubby := ToPublicKeyString(id)
if len(id) > 24 { if len(id) > 24 {
id = id[:24] id = id[:24]

View File

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