diff --git a/cmd/frisbii/main.go b/cmd/frisbii/main.go index 1bf11a0..f5405bd 100644 --- a/cmd/frisbii/main.go +++ b/cmd/frisbii/main.go @@ -100,7 +100,7 @@ func action(c *cli.Context) error { unixfsnode.AddUnixFSReificationToLinkSystem(&lsys) lsys.SetReadStorage(multicar) - server, err := frisbii.NewFrisbiiServer(ctx, logWriter, lsys, config.MaxResponseDuration, config.MaxResponseBytes, config.Listen) + server, err := frisbii.NewFrisbiiServer(ctx, logWriter, lsys, config.MaxResponseDuration, config.MaxResponseBytes, config.Listen, privKey) if err != nil { return err } diff --git a/frisbii.go b/frisbii.go index 4f2661a..b4d0a0c 100644 --- a/frisbii.go +++ b/frisbii.go @@ -10,6 +10,7 @@ import ( "github.com/ipfs/go-cid" "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/ipld/go-ipld-prime/linking" @@ -34,6 +35,7 @@ type FrisbiiServer struct { listener net.Listener mux *http.ServeMux indexerProvider IndexerProvider + privKey crypto.PrivKey } type IndexerProvider interface { @@ -48,6 +50,7 @@ func NewFrisbiiServer( maxResponseDuration time.Duration, maxResponseBytes int64, address string, + privKey crypto.PrivKey, ) (*FrisbiiServer, error) { listener, err := net.Listen("tcp", address) if err != nil { @@ -61,6 +64,7 @@ func NewFrisbiiServer( maxResponseBytes: maxResponseBytes, listener: listener, + privKey: privKey, }, nil } @@ -70,7 +74,7 @@ func (fs *FrisbiiServer) Addr() net.Addr { func (fs *FrisbiiServer) Serve() error { fs.mux = http.NewServeMux() - fs.mux.Handle("/ipfs/", NewHttpIpfs(fs.ctx, fs.logWriter, fs.lsys, fs.maxResponseDuration, fs.maxResponseBytes)) + fs.mux.Handle("/ipfs/", NewHttpIpfs(fs.ctx, fs.logWriter, fs.lsys, fs.maxResponseDuration, fs.maxResponseBytes, fs.privKey)) server := &http.Server{ Addr: fs.Addr().String(), BaseContext: func(listener net.Listener) context.Context { return fs.ctx }, diff --git a/httpipfs.go b/httpipfs.go index a12e0cd..b9805f7 100644 --- a/httpipfs.go +++ b/httpipfs.go @@ -2,6 +2,8 @@ package frisbii import ( "context" + "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -15,6 +17,7 @@ import ( "github.com/ipfs/go-unixfsnode" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/linking" + "github.com/libp2p/go-libp2p/core/crypto" ) var _ http.Handler = (*HttpIpfs)(nil) @@ -23,6 +26,12 @@ type ErrorLogger interface { LogError(status int, err error) } +type RequestSignature struct { + RequestId string `json:"requestId"` + Cid string `json:"cid"` + Protocol string `json:"protocol"` +} + // HttpIpfs is an http.Handler that serves IPLD data via HTTP according to the // Trustless Gateway specification. type HttpIpfs struct { @@ -31,6 +40,7 @@ type HttpIpfs struct { lsys linking.LinkSystem maxResponseDuration time.Duration maxResponseBytes int64 + privKey crypto.PrivKey } func NewHttpIpfs( @@ -39,6 +49,7 @@ func NewHttpIpfs( lsys linking.LinkSystem, maxResponseDuration time.Duration, maxResponseBytes int64, + privKey crypto.PrivKey, ) *HttpIpfs { return &HttpIpfs{ @@ -47,6 +58,7 @@ func NewHttpIpfs( lsys: lsys, maxResponseDuration: maxResponseDuration, maxResponseBytes: maxResponseBytes, + privKey: privKey, } } @@ -119,6 +131,27 @@ func (hi *HttpIpfs) ServeHTTP(res http.ResponseWriter, req *http.Request) { selNode := unixfsnode.UnixFSPathSelectorBuilder(path.String(), dagScope.TerminalSelectorSpec(), false) + sig := RequestSignature{ + RequestId: req.Header.Get("X-Request-Id"), + Cid: rootCid.String(), + Protocol: "https", + } + b, err := json.Marshal(sig) + if err != nil { + logError(http.StatusInternalServerError, err) + return + } + sigSigned, err := hi.privKey.Sign(b) + if err != nil { + logError(http.StatusInternalServerError, err) + return + } + attestation := fmt.Sprintf( + "\"%s.%s\"", + base64.StdEncoding.EncodeToString(b), + base64.StdEncoding.EncodeToString(sigSigned), + ) + bytesWrittenCh := make(chan struct{}) writer := newIpfsResponseWriter(res, hi.maxResponseBytes, func() { // called once we start writing blocks into the CAR (on the first Put()) @@ -129,6 +162,7 @@ func (hi *HttpIpfs) ServeHTTP(res http.ResponseWriter, req *http.Request) { res.Header().Set("Etag", etag(rootCid, path.String(), dagScope, includeDupes)) res.Header().Set("X-Content-Type-Options", "nosniff") res.Header().Set("X-Ipfs-Path", "/"+datamodel.ParsePath(req.URL.Path).String()) + res.Header().Add("Trailer", "X-Attestation") close(bytesWrittenCh) }) @@ -145,6 +179,8 @@ func (hi *HttpIpfs) ServeHTTP(res http.ResponseWriter, req *http.Request) { } logger.Debugw("error streaming CAR", "cid", rootCid, "err", err) } + + res.Header().Set("X-Attestation", attestation) } var _ io.Writer = (*ipfsResponseWriter)(nil)