Skip to content

Commit 09eaab6

Browse files
authored
Merge pull request #11 from bwesterb/lvalenta/mtcd
Add http module and mtcd server
2 parents 54195b4 + 8302f3a commit 09eaab6

File tree

9 files changed

+347
-28
lines changed

9 files changed

+347
-28
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ for improvement.
4242
To play around with MTC, you can install the `mtc` commandline tool:
4343

4444
```
45-
$ go install github.com/bwesterb/mtc/cmd/mtc@v0.1.1
45+
$ go install github.com/bwesterb/mtc/cmd/mtc@v0.1.2
4646
```
4747

4848
### Assertions
@@ -386,3 +386,54 @@ authentication path
386386

387387
This is indeed the root of the `0`th batch, and so this certificate is valid.
388388

389+
### Spin up an HTTP server with `mtcd`
390+
391+
Run an HTTP server in the background to serve static files, accept queue
392+
requests, periodically issue new batches of certificate, and serve issued
393+
certificates.
394+
395+
```
396+
$ go install github.com/bwesterb/mtc/cmd/mtcd@v0.1.2
397+
```
398+
399+
Start the server.
400+
```
401+
$ mtcd --listen-addr 8080 --ca-path .
402+
```
403+
404+
Get and inspect CA parameters.
405+
```
406+
$ curl -X GET "http://localhost:8080/static/mtc/v1/ca-params" -o ca-params
407+
$ mtc inspect ca-params ca-params
408+
issuer 123.4.5
409+
start_time 1739593848 2025-02-14 23:30:48 -0500 EST
410+
batch_duration 300 5m0s
411+
life_time 3600 1h0m0s
412+
storage_window_size 24 2h0m0s
413+
validity_window_size 12
414+
http_server localhost:8080
415+
public_key fingerprint ml-dsa-87:be1903a366b462b7b4e0010120d4b38279bbf4e350559b95e93671dbc4b821fc
416+
```
417+
418+
Queue up the assertion created in above.
419+
```
420+
$ curl -X POST "http://localhost:8080/ca/queue" --data-binary "@my-assertion" -w "%{http_code}"
421+
200
422+
```
423+
424+
Wait 5 minutes for the next batch and retrieve the certificate from the CA server.
425+
```
426+
$ curl -X POST "http://localhost:8080/ca/cert" --data-binary "@my-assertion" -o my-cert
427+
$ mtc inspect -ca-params ca-params cert my-cert
428+
subject_type TLS
429+
signature_scheme p256
430+
public_key_hash f3ee2efd8bc3dd01ab924d12ee0bc5661866a4b147fcdc6881b5455c9084c973
431+
dns [example.com]
432+
ip4 [198.51.100.60]
433+
proof_type merkle_tree_sha256
434+
CA OID 123.4.5
435+
Batch number 991
436+
index 0
437+
recomputed root 27a893fa1f864f97b2c2073f784bfd6bbcd1b2deb3d8aafbdd18b09ac10bc430
438+
authentication path
439+
```

ca/ca.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error {
946946

947947
defer treeW.Close()
948948

949-
err = tree.WriteTo(treeW)
949+
_, err = tree.WriteTo(treeW)
950950
if err != nil {
951951
return fmt.Errorf("writing out %s: %w", treePath, err)
952952
}

cmd/mtc/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"errors"
5+
56
"github.com/bwesterb/mtc"
67
"github.com/bwesterb/mtc/ca"
78
"github.com/urfave/cli/v2"
@@ -126,7 +127,7 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) {
126127
if cc.String("checksum") != "" {
127128
checksum, err = hex.DecodeString(cc.String("checksum"))
128129
if err != nil {
129-
fmt.Errorf("Parsing checksum: %w", err)
130+
return nil, fmt.Errorf("parsing checksum: %w", err)
130131
}
131132
}
132133

cmd/mtcd/main.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
"os/signal"
10+
gopath "path"
11+
"syscall"
12+
"time"
13+
14+
"github.com/bwesterb/mtc"
15+
"github.com/bwesterb/mtc/ca"
16+
"github.com/bwesterb/mtc/http"
17+
"golang.org/x/sync/errgroup"
18+
)
19+
20+
func main() {
21+
var path, listenAddr string
22+
23+
flag.StringVar(&path, "ca-path", ".", "the path to the CA state. Defaults to the current directory.")
24+
flag.StringVar(&listenAddr, "listen-addr", "", "the TCP address for the server to listen on, in the form 'host:port'.")
25+
flag.Parse()
26+
27+
if listenAddr == "" {
28+
var p mtc.CAParams
29+
buf, err := os.ReadFile(gopath.Join(path, "www", "mtc", "v1", "ca-params"))
30+
if err != nil {
31+
slog.Error("failed to read ca-params", slog.Any("err", err))
32+
os.Exit(1)
33+
}
34+
if err := p.UnmarshalBinary(buf); err != nil {
35+
slog.Error("failed to unmarshal ca-params", slog.Any("err", err))
36+
os.Exit(1)
37+
}
38+
listenAddr = p.HttpServer
39+
}
40+
41+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGQUIT, syscall.SIGTERM)
42+
defer cancel()
43+
44+
srv := http.NewServer(path, listenAddr)
45+
46+
slog.Info("starting mtcd")
47+
48+
g, ctx := errgroup.WithContext(ctx)
49+
50+
g.Go(func() error {
51+
<-ctx.Done()
52+
slog.Info("context done, preparing to exit")
53+
if err := srv.Shutdown(ctx); err != nil {
54+
slog.Error("could not gracefully close server", slog.Any("err", err))
55+
}
56+
return nil
57+
})
58+
59+
g.Go(func() error {
60+
if err := srv.ListenAndServe(); err != nil {
61+
return fmt.Errorf("could not start server: %w", err)
62+
}
63+
return nil
64+
})
65+
66+
g.Go(func() error {
67+
h, err := ca.Open(path)
68+
if err != nil {
69+
slog.Error("could not start issuance loop", slog.Any("err", err))
70+
return nil
71+
}
72+
h.Close()
73+
if err := issue(path, ctx); err != nil {
74+
return fmt.Errorf("could not start issuance loop: %w", err)
75+
}
76+
return nil
77+
})
78+
79+
if err := g.Wait(); err != nil {
80+
slog.Info("unexpected errgroup error, exiting", slog.Any("err", err))
81+
os.Exit(1)
82+
}
83+
}
84+
85+
func issue(path string, ctx context.Context) error {
86+
h, err := ca.Open(path)
87+
if err != nil {
88+
return err
89+
}
90+
params := h.Params()
91+
err = h.Issue()
92+
if err != nil {
93+
return err
94+
}
95+
h.Close()
96+
for {
97+
batchTime := params.NextBatchAt(time.Now())
98+
now := time.Now()
99+
if batchTime.After(now) {
100+
slog.Info("Sleeping until next batch", slog.Any("at", batchTime.UTC()))
101+
select {
102+
case <-ctx.Done():
103+
return nil
104+
case <-time.After(time.Until(batchTime)):
105+
}
106+
}
107+
108+
if err = issueOnce(path); err != nil {
109+
return err
110+
}
111+
}
112+
}
113+
114+
func issueOnce(path string) error {
115+
h, err := ca.Open(path)
116+
if err != nil {
117+
return err
118+
}
119+
defer h.Close()
120+
return h.Issue()
121+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ require (
1414
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
1515
github.com/russross/blackfriday/v2 v2.1.0 // indirect
1616
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
17+
golang.org/x/sync v0.11.0
1718
golang.org/x/sys v0.30.0 // indirect
1819
)

go.sum

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
2-
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
3-
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
4-
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
51
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
62
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
73
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@@ -14,13 +10,11 @@ github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
1410
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
1511
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
1612
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
17-
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
18-
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
1913
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
2014
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
2115
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
2216
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
23-
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
24-
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
17+
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
18+
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
2519
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
2620
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

http/server.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"path"
11+
"time"
12+
13+
"github.com/bwesterb/mtc"
14+
"github.com/bwesterb/mtc/ca"
15+
)
16+
17+
type Server struct {
18+
server *http.Server
19+
}
20+
21+
func NewServer(caPath string, listenAddr string) *Server {
22+
23+
mux := http.NewServeMux()
24+
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(path.Join(caPath, "www")))))
25+
mux.HandleFunc("/ca/queue", handleCaQueue(caPath))
26+
mux.HandleFunc("/ca/cert", handleCaCert(caPath))
27+
28+
return &Server{
29+
server: &http.Server{
30+
Handler: mux,
31+
Addr: listenAddr,
32+
WriteTimeout: 15 * time.Second,
33+
ReadTimeout: 15 * time.Second,
34+
}}
35+
}
36+
37+
func (s *Server) ListenAndServe() error {
38+
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
39+
return err
40+
}
41+
return nil
42+
}
43+
44+
func (s *Server) Shutdown(ctx context.Context) error {
45+
return s.server.Shutdown(ctx)
46+
}
47+
48+
func assertionFromRequestUnchecked(r *http.Request) (*ca.QueuedAssertion, error) {
49+
50+
var (
51+
checksum []byte
52+
err error
53+
)
54+
55+
checksumParam := r.URL.Query().Get("checksum")
56+
if checksumParam != "" {
57+
checksum, err = hex.DecodeString(checksumParam)
58+
if err != nil {
59+
return nil, err
60+
}
61+
}
62+
var a mtc.Assertion
63+
switch r.Method {
64+
case http.MethodPost:
65+
body, err := io.ReadAll(r.Body)
66+
if err != nil {
67+
return nil, err
68+
}
69+
defer r.Body.Close()
70+
71+
err = a.UnmarshalBinary(body)
72+
if err != nil {
73+
return nil, err
74+
}
75+
default:
76+
return nil, fmt.Errorf("unsupported HTTP method: %v", r.Method)
77+
}
78+
79+
return &ca.QueuedAssertion{
80+
Assertion: a,
81+
Checksum: checksum,
82+
}, nil
83+
}
84+
85+
func assertionFromRequest(r *http.Request) (*ca.QueuedAssertion, error) {
86+
qa, err := assertionFromRequestUnchecked(r)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
err = qa.Check()
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
return qa, nil
97+
}
98+
99+
func handleCaQueue(path string) func(w http.ResponseWriter, r *http.Request) {
100+
return func(w http.ResponseWriter, r *http.Request) {
101+
h, err := ca.Open(path)
102+
if err != nil {
103+
http.Error(w, "failed to open CA", http.StatusInternalServerError)
104+
return
105+
}
106+
defer h.Close()
107+
qa, err := assertionFromRequest(r)
108+
if err != nil {
109+
http.Error(w, "invalid assertion", http.StatusBadRequest)
110+
return
111+
}
112+
113+
err = h.Queue(qa.Assertion, qa.Checksum)
114+
if err != nil {
115+
http.Error(w, "failed to queue assertion", http.StatusInternalServerError)
116+
return
117+
}
118+
w.WriteHeader(http.StatusOK)
119+
}
120+
}
121+
122+
func handleCaCert(path string) func(w http.ResponseWriter, r *http.Request) {
123+
return func(w http.ResponseWriter, r *http.Request) {
124+
h, err := ca.Open(path)
125+
if err != nil {
126+
http.Error(w, "failed to open CA", http.StatusInternalServerError)
127+
return
128+
}
129+
defer h.Close()
130+
qa, err := assertionFromRequest(r)
131+
if err != nil {
132+
http.Error(w, "invalid assertion", http.StatusBadRequest)
133+
return
134+
}
135+
136+
cert, err := h.CertificateFor(qa.Assertion)
137+
if err != nil {
138+
http.Error(w, "failed to get certificate for assertion", http.StatusBadRequest)
139+
return
140+
}
141+
142+
buf, err := cert.MarshalBinary()
143+
if err != nil {
144+
http.Error(w, "failed to marshal certificate", http.StatusInternalServerError)
145+
return
146+
}
147+
148+
_, err = w.Write(buf)
149+
if err != nil {
150+
http.Error(w, "failed to write response", http.StatusInternalServerError)
151+
return
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)