diff --git a/cmd/litewitness/README.md b/cmd/litewitness/README.md index 1c95e99..82a9da3 100644 --- a/cmd/litewitness/README.md +++ b/cmd/litewitness/README.md @@ -56,13 +56,17 @@ SSH_AUTH_SOCK=litewitness.sock ssh-add litewitness.pem address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected -listen string address to listen for HTTP requests (default "localhost:7380") - -Only one of `-bastion` or `-listen` must be specified. The former will cause -litewitness to serve requests through a bastion reverse proxy (see below). The -latter will listen for HTTP requests on the specified port. (HTTPS needs to be -terminated outside of litewitness.) The bastion flag is an optionally -comma-separated list of bastions to try in order until one connects -successfully. If the connection drops after establishing, litewitness exits. + -no-listen + do not open any listening socket, rely exclusively on bastions + +Only one of `-bastion` or `-listen` must be specified, or `-no-listen` can be +used to rely exclusively on per-log bastions configured in the database. The +`-bastion` flag will cause litewitness to serve requests through a bastion +reverse proxy (see below). The `-listen` flag will listen for HTTP requests on +the specified port. (HTTPS needs to be terminated outside of litewitness.) The +bastion flag is an optionally comma-separated list of bastions to try in order +until one connects successfully. If the connection drops after establishing, +litewitness exits. ## witnessctl @@ -81,6 +85,14 @@ re-added. To disable a log, remove all its keys. The `add-key` and `del-key` commands add and remove verifier keys for a known log. The name of the key must match the log origin. + witnessctl add-bastion -db -origin -bastion + witnessctl del-bastion -db -origin -bastion + +The `add-bastion` and `del-bastion` commands add and remove bastion addresses +for a log. Multiple bastions can be configured for a log and will be used +simultaneously. Bastion configuration is reloaded when litewitness receives a +SIGHUP signal. + witnessctl add-sigsum-log -db -key The `add-sigsum-log` command is a helper that adds a new Sigsum log, computing diff --git a/cmd/litewitness/litewitness.go b/cmd/litewitness/litewitness.go index 4da8575..8a703ab 100644 --- a/cmd/litewitness/litewitness.go +++ b/cmd/litewitness/litewitness.go @@ -21,6 +21,7 @@ import ( "net/http" "os" "os/signal" + "slices" "strings" "syscall" "time" @@ -39,10 +40,64 @@ var nameFlag = flag.String("name", "", "URL-like (e.g. example.com/foo) name of var dbFlag = flag.String("db", "litewitness.db", "path to sqlite database") var sshAgentFlag = flag.String("ssh-agent", "litewitness.sock", "path to ssh-agent socket") var listenFlag = flag.String("listen", "localhost:7380", "address to listen for HTTP requests") +var noListenFlag = flag.Bool("no-listen", false, "do not open any listening socket, rely exclusively on bastions") var keyFlag = flag.String("key", "", "SSH fingerprint (with SHA256: prefix) of the witness key") var bastionFlag = flag.String("bastion", "", "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected") var testCertFlag = flag.Bool("testcert", false, "use rootCA.pem for connections to the bastion") +type ConnectionSet struct { + connections map[string]func() // connection => cancel func + connect func(context.Context, string) +} + +func NewConnectionSet(connect func(context.Context, string)) *ConnectionSet { + return &ConnectionSet{ + connections: make(map[string]func()), + connect: connect, + } +} + +func (s *ConnectionSet) Configure(ctx context.Context, addrs []string) { + slices.Sort(addrs) + + // Disconnect addresses that have disappeared. + var toDelete []string + for addr, cancel := range s.connections { + if _, found := slices.BinarySearch(addrs, addr); !found { + cancel() + // Postpone delete, we can't delete while iterating over the map. + toDelete = append(toDelete, addr) + } + } + for _, addr := range toDelete { + delete(s.connections, addr) + } + + // Connect new bastions. + for _, addr := range addrs { + if _, found := s.connections[addr]; found { + continue + } + // Quit early on cancel. + if ctx.Err() != nil { + break + } + connectionCtx, cancel := context.WithCancel(ctx) + s.connections[addr] = cancel + go s.connect(connectionCtx, addr) + } +} + +func onSignal(signo os.Signal, callback func()) { + c := make(chan os.Signal, 1) + signal.Notify(c, signo) + go func() { + for range c { + callback() + } + }() +} + func main() { flag.Parse() @@ -52,18 +107,14 @@ func main() { console.SetFilter(slogconsole.IPAddressFilter) slog.SetDefault(slog.New(slogconsole.MultiHandler(h, console))) - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGUSR1) - go func() { - for range c { - slog.Info("received USR1 signal, toggling log level") - if level.Level() == slog.LevelDebug { - level.Set(slog.LevelInfo) - } else { - level.Set(slog.LevelDebug) - } + onSignal(syscall.SIGUSR1, func() { + slog.Info("received USR1 signal, toggling log level") + if level.Level() == slog.LevelDebug { + level.Set(slog.LevelInfo) + } else { + level.Set(slog.LevelDebug) } - }() + }) signer := connectToSSHAgent() @@ -89,10 +140,77 @@ func main() { BaseContext: func(net.Listener) context.Context { return ctx }, } e := make(chan error, 1) + + bastionSet := NewConnectionSet(func(ctx context.Context, addr string) { + var delays = []time.Duration{ + 100 * time.Millisecond, + 1 * time.Second, 1 * time.Second, 1 * time.Second, + 5 * time.Second, 15 * time.Second, 30 * time.Second, + 1 * time.Minute, + } + + // If a connection survives for resetRetryDelay, reset the retry delay. + const resetRetryDelay = 5 * time.Minute + + retry := 0 + for { + startTime := time.Now() + err := connectToBastion(ctx, addr, signer, srv, true) + duration := time.Since(startTime) + slog.Warn("bastion connection failed", "bastion", addr, "duration", duration, "err", err) + + // Quit early on cancel. + if ctx.Err() != nil { + return + } + + // If the connection lasted long enough, reset the retry delay. + if duration >= resetRetryDelay { + retry = 0 + } + + // Wait before retrying. + var delay time.Duration + if retry < len(delays) { + delay = delays[retry] + } else { + delay = delays[len(delays)-1] + } + slog.Info("waiting before reconnecting to bastion", "bastion", addr, "delay", delay) + timer := time.NewTimer(delay) + select { + case <-ctx.Done(): + timer.Stop() + return + case <-timer.C: + } + retry++ + } + }) + + // Handle log-specific bastions. + logBastions, err := w.AllBastions() + if err != nil { + fatal("failed looking up bastions", "err", err) + } + bastionSet.Configure(ctx, logBastions) + + // At this point, ownership of bastionSet belongs with the signal goroutine, + // and must no longer be accessed by main goroutine. + onSignal(syscall.SIGHUP, func() { + slog.Info("received SIGHUP, reconfiguring bastions") + logBastions, err := w.AllBastions() + if err != nil { + slog.Warn("failed looking up bastions", "err", err) + return + } + bastionSet.Configure(ctx, logBastions) + }) + if *bastionFlag != "" { go func() { for _, bastion := range strings.Split(*bastionFlag, ",") { - err := connectToBastion(ctx, bastion, signer, srv) + err := connectToBastion(ctx, bastion, signer, srv, false) if err == errBastionDisconnected { // Connection succeeded and then was interrupted. Restart to // let the scheduler apply any backoff, and then retry all bastions. @@ -102,11 +220,13 @@ func main() { } e <- errors.New("couldn't connect to any bastion") }() - } else { + } else if !*noListenFlag { go func() { slog.Info("listening", "addr", *listenFlag) e <- srv.ListenAndServe() }() + } else if len(logBastions) == 0 { + slog.Warn("configured to not open a listening port, but no bastions configured") } select { @@ -246,7 +366,7 @@ func indexHandler(w *witness.Witness) http.HandlerFunc { var errBastionDisconnected = errors.New("connection to bastion interrupted") -func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server) error { +func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server, logSpecific bool) error { slog.Info("connecting to bastion", "bastion", bastion) cert, err := selfSignedCertificate(signer) if err != nil { @@ -279,7 +399,19 @@ func connectToBastion(ctx context.Context, bastion string, signer *signer, srv * slog.Info("connecting to bastion failed", "bastion", bastion, "err", err) return fmt.Errorf("connecting to bastion: %v", err) } + // Ensure that the connection is closed when our context is cancelled. + ctx, cancel = context.WithCancel(ctx) + defer cancel() + go func(ctx context.Context) { + // TODO: gracefully complete in-flight requests. + <-ctx.Done() + conn.Close() + }(ctx) + slog.Info("connected to bastion", "bastion", bastion) + if logSpecific { + ctx = witness.ContextWithBastion(ctx, bastion) + } // TODO: find a way to surface the fatal error, especially since with // TLS 1.3 it might be that the bastion rejected the client certificate. (&http2.Server{ diff --git a/cmd/litewitness/testdata/logbastion.txt b/cmd/litewitness/testdata/logbastion.txt new file mode 100644 index 0000000..86932a6 --- /dev/null +++ b/cmd/litewitness/testdata/logbastion.txt @@ -0,0 +1,201 @@ +# gentest seed b4e385f4358f7373cfa9184b176f3cccf808e795baf04092ddfde9461014f0c4 +# gentest seed 8ece6c6015722b4ed02111acbc928cbf15022611be54264b70aa8458a5466f13 + +# set up log with bastion +exec witnessctl add-sigsum-log -key=ffdc2d4d98e4124d3feaf788c0c2f9abfd796083d1f0495437f302ec79cf100f +exec witnessctl add-bastion -origin=sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 -bastion=localhost:9443 + +# start bastion +exec litebastion -listen localhost:9443 -testcert -backends=backends.txt &litebastion& +waitfor localhost:9443 + +# start ssh-agent +env SSH_AUTH_SOCK=$WORK/s # barely below the max path length +! exec ssh-agent -a $SSH_AUTH_SOCK -D & # ssh-agent always exits 2 +waitfor $SSH_AUTH_SOCK +chmod 600 witness_key.pem +exec ssh-add witness_key.pem + +# start litewitness +exec litewitness -ssh-agent=$SSH_AUTH_SOCK -name=example.com/witness -testcert -key=e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a -no-listen &litewitness& +waitfor https://localhost:9443/e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a/ + +# add-checkpoint +exec hurl --cacert rootCA.pem --test add-checkpoint.hurl + +# start a second bastion on a different port +exec litebastion -listen localhost:9444 -testcert -backends=backends.txt &litebastion2& +waitfor localhost:9444 + +# add a second log and configure it to use the second bastion +exec witnessctl add-sigsum-log -key=7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 +exec witnessctl add-bastion -origin=example.org/v1/tree/7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 -bastion=localhost:9444 + +# reload litewitness to pick up new log and bastion +killall -SIGHUP litewitness +waitfor https://localhost:9444/e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a/ + +# test that accessing a log configured for bastion 9444 through bastion 9443 fails with 403 +! exec hurl --cacert rootCA.pem --test add-checkpoint-second-log.hurl + +# check that litewitness shut down cleanly +killall +wait litewitness +stderr 'shutting down' + +# check the litebastion output +wait litebastion +stderr 'msg="accepted new backend connection" backend=e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a' + +# witnessctl list-logs +exec witnessctl list-logs +stdout sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 +stdout "size":1 + + +-- backends.txt -- +f97f12534ff2478cfc36b00d09a85d4faeb6589ac19a0895c348a499627c531c +e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a + + +-- witness_key.pem -- +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz +c2gtZWQyNTUxOQAAACBkhIrYq+1uhZgbOzh1slK4dn67SwL3A6yjsecbvWqOUAAA +AIgN5+09DeftPQAAAAtzc2gtZWQyNTUxOQAAACBkhIrYq+1uhZgbOzh1slK4dn67 +SwL3A6yjsecbvWqOUAAAAEAx/8IRbsvgA6yqgAq3B1e9fVMgbj/r72ptB5bZVTCz +T2SEitir7W6FmBs7OHWyUrh2frtLAvcDrKOx5xu9ao5QAAAAAAECAwQF +-----END OPENSSH PRIVATE KEY----- + + +-- localhost.pem -- +-----BEGIN CERTIFICATE----- +MIID/TCCAmWgAwIBAgIUPCwTk8pjBLPr+3czZAaGcg8RuDowDQYJKoZIhvcNAQEL +BQAwNjELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB1Rlc3QgQ0ExFTATBgNVBAMMDFRl +c3QgUm9vdCBDQTAgFw0yNTA5MzAxMjAzMDdaGA8yMDU1MDkyMzEyMDMwN1owMDEL +MAkGA1UEBhMCVVMxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9zdDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKmcTiZ4FX92VKuQU9zioRv7 +nqsdEmMoOKs1nVJd7myLMi/02flEbDAmtDkh4LFPuiNw2OaqphBYM4MlZCxofKrL ++/Skb763IOA5Ly97QGrOzYBtksPJlAPlF8pvRJO22ZIlBFpZUsivOZnFyhnvhFki +TMMDpvNqtAAIee3Hv0y4EolhausAecaErlPc510LmcoU+VF5bYgur68nLzSvetzG +qKblYkhFi3pEAJ7wcFUWZbFlNKDQVB+o4aEnqGj5mvYIbsh8UNJIqANA5qgxjoGc +8F5eqWlXmFA9+R9lRe5JW6Ia91V8eKsTCRKuuFvH2gIs8pxrzV3fvdrXXHJCzxUC +AwEAAaOBhjCBgzAUBgNVHREEDTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADALBgNV +HQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFO0cjjCGyrIC +6ov0YT60AlyXsFRsMB8GA1UdIwQYMBaAFA3F8nWmTSb58CraZhp+Ur63NaC7MA0G +CSqGSIb3DQEBCwUAA4IBgQA4EzY7MH95cFlVzbhk1loSlPRgDaWBga1y7U17jOnx +ulqGr6ySInAounhZPDFfwIxyAbZE4N/Bq1omkfI+osvGBbkUdj7Z7ktllj5pON0t +gdmCq/S1Q/tEcBvaS1FjR+AMu2enUVKn6mMXgjyj4xZNutqbXfKD4wdL1qP2KLbZ +M832TqI2tVUVOGSq0zfsPe5sDD/NHSvkKaeM+ZGlB955fzBsICOx4W5mcz/Zhfk5 +gbJZsNPVlt8al3/iO0AIOGXzZOnlo8baHmh4TJH+uRYgHh8KFAvA8rebStg+Rn5w +j+uAFgG+gf7JSVo6bjLWMkmkN6ooJFYMwwXav83STOU0TAQdqX+YM97Tke+U3DEd +MqeX+hS+LyXGG0hHardo9xrRs1/W9xP453ybm2KFPODPyPSTuyOSUzleIeRGfmXg +Ri++XfeZMNcPYNAj0joJEmoszmY17vuoBDidhYVQE+g+4uxhJeGROWvNz2ONqxqw +QTyv4nS0hwl7T0VCoBC+bSg= +-----END CERTIFICATE----- + + +-- localhost-key.pem -- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpnE4meBV/dlSr +kFPc4qEb+56rHRJjKDirNZ1SXe5sizIv9Nn5RGwwJrQ5IeCxT7ojcNjmqqYQWDOD +JWQsaHyqy/v0pG++tyDgOS8ve0Bqzs2AbZLDyZQD5RfKb0STttmSJQRaWVLIrzmZ +xcoZ74RZIkzDA6bzarQACHntx79MuBKJYWrrAHnGhK5T3OddC5nKFPlReW2ILq+v +Jy80r3rcxqim5WJIRYt6RACe8HBVFmWxZTSg0FQfqOGhJ6ho+Zr2CG7IfFDSSKgD +QOaoMY6BnPBeXqlpV5hQPfkfZUXuSVuiGvdVfHirEwkSrrhbx9oCLPKca81d373a +11xyQs8VAgMBAAECggEAH3wF38s7xmDrX7uXbbHeEUk4j3ACmUh+mH2H2iHYn+qI +4vETQ1vJr3iHzPE2egOgPHL2uH7l+7O7wDUBLuMofTYHa8bYfXEWF7lVwn0hHJKO +ADCW5WQ2ZzCwJWJZOwhew+u+Lp1VKi6oxRw7o2vcSAV/dVXouFfO2RC5vYNuRem5 +m/3xaOlDpPgmhkRV/I+AJk0f8zNSWpU+izonxqgSINREyemWVY705s+HirMEQgRV +GN0ftwNGuz7/5rk8eH1iAGsTK9NEi77hCtyUKYhOC8NWzxGgS2mhqHslms8M2ZOm +rBiZZ5Sh5U53r6IWCIKqG92fzFZ/fcSZGUDHrcjcYQKBgQDptvr/f21Ze9OCrGmk +PktnM5v0DiP0z0Ywf28oz9rTneletJqQTsw6uP/GnIQrXwnaTWICjJtiMp9wxdM+ +9drKhZufImC0NGTLz1vJxLR5x5ao8T/juPIxm7TLfH5XjZMJgRMD1D5IvW8k9hWU +1yDJxNCx1ikY7N3FP1XwmFrhRQKBgQC5yIeeABqXCy2A/wYrWqZ3zYFWe4t6Qqzm +afuoUGaqXK9GjgDemOlUKzhUb08tnPNXzKJagqmM4F0zKgcwq61ECH3SliMt82PL +1i1KTB2bUSXnwXOsbQyWVXz7YuVsWYakdHt6Vbbmy1JexRhfV3q5HCuj1fElNLTv +YheLV0JLkQKBgEFuhR764fZngHPZKUpeVmXyQPs26kIjtZbmVoyqhK0yTJ/DGHLG +XM8j9Bf6wdYSqYOAnqvwCaCYY6MC/31k/3grp8IJseFBueaFi0EV3SErC7cIs8Zh +hQz2dstxcz232S6UAGrWBQoAXxmN+8TL5dYXUAY52w+rYPtUHA9b2DWxAoGAUlBL +7jBjl5qnPalApYLTkO8nqBazFKdoDerVSpzc8AyCyELwla+wac+AdMCglzgcBUGw +iWOtFbLu+FVdvC3EZglRHjXRPnHBPLYXePzCfWd14PowcywZ0J3t8z+9IMWFx2Wo +s+o4UIezZjPzeYK76DpYB44p+u8gX5PZlK5DvFECgYBAKrvWEOy8J29QB3er+ERv +qvmczAvfJWWgheKMxQhx/3pu6UW/v/hmcKngMWjr/XqxFKp05ca1VBCmPB3s0776 +IMJnvyTUFs5S7COpPJOC81JB/28UGQFcAZ+Kiwd1sx5gd1Q+EnaAhyxplWi/m6Zv +tKfWSmtc1XhMiUpw331jNg== +-----END PRIVATE KEY----- + + +-- rootCA.pem -- +-----BEGIN CERTIFICATE----- +MIIETzCCAregAwIBAgIUauyztCDIDrmE46pw/Pg5nn7U2gUwDQYJKoZIhvcNAQEL +BQAwNjELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB1Rlc3QgQ0ExFTATBgNVBAMMDFRl +c3QgUm9vdCBDQTAgFw0yNTA5MzAxMjAyMzFaGA8yMDU1MDkyMzEyMDIzMVowNjEL +MAkGA1UEBhMCVVMxEDAOBgNVBAoMB1Rlc3QgQ0ExFTATBgNVBAMMDFRlc3QgUm9v +dCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKHLd07/qunxEFL8 +BDCk1J+2m/TcUM0oG6dFVZjf/4+xSDH3F8tA6ZeqBLriQRBTuqwFBZVvoJwqJjip +CAVzWkvvyiuvAeywJNXIe7kqmmDeAEoq2Z/F9j6p5y8+ddy7VX6pTdsVYOyGsBCx +GZb7tuj0mtLl18HuezH0P7prDHBuNx+FjMblHXr5yslQ95d53v4dmI8ONObhS2ju +ltgveHzaWbssaAqoVwfWJfWJtc0C3IgGh8MgFhhdzTQ4DvHxYLjwGB0eE/Vpu757 +B4pvYcT7JjBtjv8VyhW62ZPZNBlNjFOtFHhxZcMGDiOsyy9OgwIzzvy9UMg0WisZ +OxZmRkASuApoMRftSg2ZmTG5THdFgEQD6pqE3llmhO57ZTt1DvPNUlUh8ND9u9Ov +TcCjEeBXJRRjx7tUhob3mU+tYAi9DbXX/Wje0wBaRKh073wzRoAsFU9PspgKsZq4 +JNDFSey4DK8W/O8jYvmNfuQ3TJwiAWxNUDDUdGN3TwnaKRQesQIDAQABo1MwUTAd +BgNVHQ4EFgQUDcXydaZNJvnwKtpmGn5Svrc1oLswHwYDVR0jBBgwFoAUDcXydaZN +JvnwKtpmGn5Svrc1oLswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AYEAK4/oXvmhohTM10N+ZpaHDC8S+xr3z2tayJiNOrzzVE209MEOMCYlEz9IeyUx +q5rmD4SJRLmyR8DpkO3dO4MjZYv2Nq+NS2kb5PmhUKmIEcwfFmXHjX/FCqPVwdH9 +yM63o3GS1PhbQ6vzK4NMsOUT3nvnD87gFvXKOSn+gpETmYNA55wD/Qcu8LhqDk0I +YDrXiAja++gEf7/RZ5oPYNXi8t1+PjUDhP8J/tz9G90MZBR/FztTVIGTdpIoVwDB +mMpAHW/7S9hEwVlZ69CUMfrm0uG68xaJPNH1MmaZdEdI0PnSU2nVwLLLqsYQmZqp ++AGOq+6qOcvSMaFIJ3OLPUKqgn40f5J7I6retaER53Rahe/15Hy8H/sRsZQFPjeW +qi86vkgd5GFSu2GNHN/Mhq7dHWq9U0mNO+BZt/G6du3gJ+SaO1UkJhzpNiRY37sR +FU3HWF4uYVCt+OHqJ4we9XUKT80Y4ejNYkG25H+9DOy/Gtwb4qquqBoZYRUUThA1 +kJHt +-----END CERTIFICATE----- + + +-- add-checkpoint.hurl -- +POST https://localhost:9443/e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a/add-checkpoint +``` +old 0 + +sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 +1 +KgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +— sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 UgIom7fPZTqpxWWhyjWduBvTvGVqsokMbqTArsQilegKoFBJQjUFAmQ0+YeSPM3wfUQMFSzVnnNuWRTYrajXpNUbIQY= +``` +HTTP 200 +[Asserts] +body contains "— example.com/witness" + + +-- add-checkpoint-second-log.hurl -- +# This test verifies that a request coming through bastion A (9443) for a log configured +# with bastion B (9444) is rejected with a 403 +POST https://localhost:9443/e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a/add-checkpoint +``` +old 0 + +sigsum.org/v1/tree/7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 +1 +KgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +— sigsum.org/v1/tree/7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 +``` +HTTP 403 +[Asserts] +body contains "wrong bastion" + +POST https://localhost:9444/e933707e0e36c30f01d94b5d81e742da373679d88eb0f85f959ccd80b83b992a/add-checkpoint +``` +old 0 + +sigsum.org/v1/tree/7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 +1 +KgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +— sigsum.org/v1/tree/7c589ec3432005b91812adef062f4783ce95c6ff502048ce67063c1b84062e34 +``` +HTTP 200 diff --git a/cmd/litewitness/testdata/loglist.txt b/cmd/litewitness/testdata/loglist.txt index 6f34c00..7c16da2 100644 --- a/cmd/litewitness/testdata/loglist.txt +++ b/cmd/litewitness/testdata/loglist.txt @@ -18,6 +18,11 @@ exec witnessctl list-logs cmp stdout list-logs.jsonl ! stderr . +# add a bastion +exec witnessctl add-bastion -origin sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120 -bastion bastion.example.org:666 +exec witnessctl list-logs +cmp stdout list-logs.2.jsonl +! stderr . -- log_list.0 -- # @@ -69,6 +74,10 @@ vkey sigsum.org/v1/tree/80f121ea8f6364ca8c19212d7caf4e235d00aa34a98d86b60204c24f origin sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702 -- list-logs.jsonl -- -{"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"]} -{"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"]} -{"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"]} +{"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"],"bastions":[]} +{"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"],"bastions":[]} +{"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"],"bastions":[]} +-- list-logs.2.jsonl -- +{"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"],"bastions":[]} +{"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"],"bastions":[]} +{"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"],"bastions":["bastion.example.org:666"]} diff --git a/cmd/witnessctl/witnessctl.go b/cmd/witnessctl/witnessctl.go index 4d2877e..8e16e73 100644 --- a/cmd/witnessctl/witnessctl.go +++ b/cmd/witnessctl/witnessctl.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "os" "strings" @@ -26,6 +27,8 @@ func usage() { fmt.Println(" add-log -db -origin ") fmt.Println(" add-key -db -origin -key ") fmt.Println(" del-key -db -origin -key ") + fmt.Println(" add-bastion -db -origin -bastion ") + fmt.Println(" del-bastion -db -origin -bastion ") fmt.Println(" add-sigsum-log -db -key ") fmt.Println(" pull-logs -db -source [-verbose]") fmt.Println(" list-logs -db ") @@ -63,6 +66,23 @@ func main() { delKey(db, *originFlag, *keyFlag) log.Printf("Deleted key %q for log %q.", *keyFlag, *originFlag) + case "add-bastion": + originFlag := fs.String("origin", "", "log name") + bastionFlag := fs.String("bastion", "", "address:port") + fs.Parse(os.Args[2:]) + checkBastion(*bastionFlag) + db := openDB(*dbFlag) + addBastion(db, *originFlag, *bastionFlag) + log.Printf("Added bastion %q for log %q.", *bastionFlag, *originFlag) + + case "del-bastion": + originFlag := fs.String("origin", "", "log name") + bastionFlag := fs.String("bastion", "", "address:port") + fs.Parse(os.Args[2:]) + db := openDB(*dbFlag) + delBastion(db, *originFlag, *bastionFlag) + log.Printf("Deleted bastion %q for log %q.", *bastionFlag, *originFlag) + case "add-sigsum-log": keyFlag := fs.String("key", "", "hex-encoded key") fs.Parse(os.Args[2:]) @@ -112,6 +132,12 @@ func checkKeyMatches(origin string, vk string) { } } +func checkBastion(bastion string) { + if _, _, err := net.SplitHostPort(bastion); err != nil { + log.Fatalf("Error parsing bastion %q as address:port: %v", bastion, err) + } +} + func addKey(db *sqlite.Conn, origin string, vk string) { err := sqlitexExec(db, "INSERT INTO key (origin, key) VALUES (?, ?)", nil, origin, vk) if err != nil { @@ -129,6 +155,23 @@ func delKey(db *sqlite.Conn, origin string, vk string) { } } +func addBastion(db *sqlite.Conn, origin string, bastion string) { + err := sqlitexExec(db, "INSERT INTO bastion (origin, bastion) VALUES (?, ?)", nil, origin, bastion) + if err != nil { + log.Fatalf("Error adding bastion: %v", err) + } +} + +func delBastion(db *sqlite.Conn, origin string, bastion string) { + err := sqlitexExec(db, "DELETE FROM bastion WHERE origin = ? AND bastion = ?", nil, origin, bastion) + if err != nil { + log.Fatalf("Error deleting bastion: %v", err) + } + if db.Changes() == 0 { + log.Fatalf("Bastion %q not found.", bastion) + } +} + func addSigsumLog(db *sqlite.Conn, keyFlag string) { if len(keyFlag) != sigsum.PublicKeySize*2 { log.Fatal("Key must be 32 hex-encoded bytes.") @@ -230,10 +273,18 @@ func listLogs(db *sqlite.Conn) { 'origin', log.origin, 'size', log.tree_size, 'root_hash', log.tree_hash, - 'keys', json_group_array(key.key)) + 'keys', COALESCE( + json_group_array(key.key) FILTER (WHERE key.key IS NOT NULL), + json_array() + ), + 'bastions', COALESCE( + json_group_array(bastion.bastion) FILTER (WHERE bastion.bastion IS NOT NULL), + json_array() + )) FROM log LEFT JOIN key on log.origin = key.origin + LEFT JOIN bastion on log.origin = bastion.origin GROUP BY log.origin ORDER BY diff --git a/internal/witness/witness.go b/internal/witness/witness.go index 16d2a59..a08a2c2 100644 --- a/internal/witness/witness.go +++ b/internal/witness/witness.go @@ -2,12 +2,14 @@ package witness import ( "bytes" + "context" "crypto" "errors" "fmt" "io" "log/slog" "net/http" + "slices" "strconv" "strings" "sync" @@ -52,6 +54,11 @@ func OpenDB(dbPath string) (*sqlite.Conn, error) { key TEXT NOT NULL, -- note verifier key FOREIGN KEY(origin) REFERENCES log(origin) ); + CREATE TABLE IF NOT EXISTS bastion ( + origin TEXT NOT NULL, + bastion TEXT NOT NULL, -- addr:port + FOREIGN KEY(origin) REFERENCES log(origin) + ); `) } @@ -90,6 +97,17 @@ func (w *Witness) VerifierKey() string { return w.s.Verifier().String() } +func (w *Witness) AllBastions() ([]string, error) { + var bastions []string + err := w.dbExec("SELECT DISTINCT bastion FROM bastion", + func(stmt *sqlite.Stmt) error { + bastions = append(bastions, stmt.GetText("bastion")) + return nil + }) + return bastions, err + +} + type conflictError struct { known int64 } @@ -97,10 +115,24 @@ type conflictError struct { func (*conflictError) Error() string { return "known tree size doesn't match provided old size" } var errUnknownLog = errors.New("unknown log") +var errWrongBastion = errors.New("rejected request, bad/missing bastion") var errInvalidSignature = errors.New("invalid signature") var errBadRequest = errors.New("invalid input") var errProof = errors.New("bad consistency proof") +type viaBastionKey struct{} + +func ContextWithBastion(ctx context.Context, bastion string) context.Context { + return context.WithValue(ctx, viaBastionKey{}, bastion) +} + +func ContextBastion(ctx context.Context) string { + if bastion, ok := ctx.Value(viaBastionKey{}).(string); ok { + return bastion + } + return "" +} + func (w *Witness) serveAddCheckpoint(rw http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { @@ -108,7 +140,8 @@ func (w *Witness) serveAddCheckpoint(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusInternalServerError) return } - cosig, err := w.processAddCheckpointRequest(body) + + cosig, err := w.processAddCheckpointRequest(body, ContextBastion(r.Context())) if err, ok := err.(*conflictError); ok { rw.Header().Set("Content-Type", "text/x.tlog.size") rw.WriteHeader(http.StatusConflict) @@ -116,7 +149,7 @@ func (w *Witness) serveAddCheckpoint(rw http.ResponseWriter, r *http.Request) { return } switch err { - case errUnknownLog, errInvalidSignature: + case errUnknownLog, errInvalidSignature, errWrongBastion: http.Error(rw, err.Error(), http.StatusForbidden) return case errBadRequest: @@ -135,7 +168,7 @@ func (w *Witness) serveAddCheckpoint(rw http.ResponseWriter, r *http.Request) { } } -func (w *Witness) processAddCheckpointRequest(body []byte) (cosig []byte, err error) { +func (w *Witness) processAddCheckpointRequest(body []byte, bastion string) (cosig []byte, err error) { l := w.log.With("request", string(body)) defer func() { if err != nil { @@ -169,6 +202,14 @@ func (w *Witness) processAddCheckpointRequest(body []byte) (cosig []byte, err er } origin, _, _ := strings.Cut(string(noteBytes), "\n") l = l.With("origin", origin) + bastions, err := w.getBastions(origin) + if err != nil { + return nil, err + } + if bastion != "" && !slices.Contains(bastions, bastion) { + l.Debug("rejected request from unexpected bastion", "bastion", bastion) + return nil, errWrongBastion + } verifier, err := w.getKeys(origin) if err != nil { return nil, err @@ -300,6 +341,16 @@ func (w *Witness) getKeys(origin string) (note.Verifiers, error) { return note.VerifierList(verifiers...), nil } +func (w *Witness) getBastions(origin string) ([]string, error) { + var bastions []string + err := w.dbExec("SELECT bastion FROM bastion WHERE origin = ?", + func(stmt *sqlite.Stmt) error { + bastions = append(bastions, stmt.GetText("bastion")) + return nil + }, origin) + return bastions, err +} + func (w *Witness) dbExec(query string, resultFn func(stmt *sqlite.Stmt) error, args ...interface{}) error { w.dmMu.Lock() defer w.dmMu.Unlock() diff --git a/internal/witness/witness_test.go b/internal/witness/witness_test.go index cccdaf4..b97d238 100644 --- a/internal/witness/witness_test.go +++ b/internal/witness/witness_test.go @@ -40,7 +40,7 @@ sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e KgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= — sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 UgIom7fPZTqpxWWhyjWduBvTvGVqsokMbqTArsQilegKoFBJQjUFAmQ0+YeSPM3wfUQMFSzVnnNuWRTYrajXpNUbIQY= -`)) +`), "") fatalIfErr(t, err) // Stall the first request updating to the shorter size between getting @@ -63,7 +63,7 @@ sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e RcCI1Nk56ZcSmIEfIn0SleqtV7uvrlXNccFx595Iwl0= — sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 UgIom2VbtIcdFbwFAy1n7s6IkAxIY6J/GQOTuZF2ORV39d75cbAj2aQYwyJre36kezNobZs4SUUdrcawfAB8WVrx6go= -`)) +`), "") if _, ok := err.(*conflictError); !ok { t.Errorf("expected conflict, got %v", err) } @@ -87,7 +87,7 @@ sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e QrtXrQZCCvpIgsSmOsah7HdICzMLLyDfxToMql9WTjY= — sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 UgIomw/EOJmWi0i1FQsOj+etB7F8IccFam/jgd6wzRns4QPVmyEZtdvl1U2KEmLOZ/ASRcWJi0tW90dJWAShei7sDww= -`)) +`), "") if err != nil { t.Errorf("racing request failed: %v", err) }