Running a Verifiable WebAssembly Application Stored in immudb
During one of our internal meetings, we discussed the idea of running Wasm web applications in the browser that are ...
During one of our internal meetings, we discussed the idea of running Wasm web applications in the browser that are stored tamperproof and client-verifiable in immudb. This blog post describes how a simple Wasm application (written in Go) can be built, deployed, and made available from an immudb server. The goal is for the Wasm application to be executed at runtime inside the web server.
By serving the application in this way, the provider can trust the fact that the application’s consumer will always get a deterministic result while running his application. This is achieved by the combination of immudb’s verified methods and thanks to the wasi runtime interpreter (there will be no interference by the operating system).
It’s like an immutable application!
What is WebAssembly (Wasm)?
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
What is immudb?
immudb is an immutable, tamper-proof database with built-in cryptographic proof and verification. It tracks changes in sensitive data and the integrity of the history is protected by the clients, without the need to trust the database. It can operate both as a key-value store and/or as relational database (SQL). To quickly run immudb using docker run the following command:
docker run -d -p 3322:3322 -it --rm --name immudb codenotary/immudb:latest
A Simple Wasm Application in Go
The server is completely agnostic to what will be executed. This means that there is no need for any special configuration or dependencies on the server. The code is self-contained in the wasi binary. So it’s extremely simple to execute something.
Lets start with a very simple WebAssembly application that returns the sum of 2 arguments.
simple.go
package main
func main() {}
//export Sum
func Sum(a, b uint32) uint32 {
return a+b
}
Compile the application by running:
tinygo build -o sum.wasm -target wasi .
This will output the sum.wasm file that will be stored in immudb (using default settings in the example) using the following code. Loader.go will store the Wasm application under the key/value entry sum
.
loader.go
package main
import (
"context"
"github.com/codenotary/immudb/pkg/client"
"io/ioutil"
"log"
"os"
"path"
)
func main() {
argsWithoutProg := os.Args[1:]
if len(argsWithoutProg) != 1 {
log.Fatal("Usage: loader {wasmexecutable}")
}
c, err := client.NewImmuClient(client.DefaultOptions())
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// login with default username and password
_, err = c.Login(ctx, []byte(`immudb`), []byte(`immudb`))
if err != nil {
log.Fatal(err)
}
filename := argsWithoutProg[0]
content, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
_, err = c.VerifiedSet(ctx, []byte(path.Base(filename)), content)
if err != nil {
log.Fatal(err)
}
}
Now build and execute the loader application and point to the sum.wasm
file:
go run ./loader.go sum.wasm
Provide the Wasm Application
Now we need a simple web server application that will run the Wasm application that is stored in immudb and serves it to the browser client. The web server can dynamically launch the application with the parameters provided by the user. The wasi application is fetched from immudb and executed at runtime by the interpreter.
Let’s serve the WebAssembly application main.go:
package main
import (
"context"
"fmt"
"github.com/codenotary/immudb/pkg/client"
wasmer "github.com/wasmerio/wasmer-go/wasmer"
"log"
"net/http"
"strconv"
"strings"
)
func main() {
e := executor{}
if err := e.connect(); err != nil {
log.Fatal(err)
}
http.HandleFunc("/immutable/", e.executeHandler)
fmt.Printf("verified WebAssembly app running: 8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}
type executor struct {
client client.ImmuClient
}
func (e *executor) connect() error {
c, err := client.NewImmuClient(client.DefaultOptions())
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// login with default username and password
_, err = c.Login(ctx, []byte(`immudb`), []byte(`immudb`))
if err != nil {
return err
}
e.client = c
return nil
}
func (e *executor) executeHandler(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/immutable/")
entry, err := e.client.VerifiedGet(context.TODO(), []byte(key))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
store := wasmer.NewStore(wasmer.NewEngine())
module, err := wasmer.NewModule(store, entry.Value)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
wasiEnv, err := wasmer.NewWasiStateBuilder("wasi-program").
Finalize()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
importObject, err := wasiEnv.GenerateImportObject(store, module)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
instance, err := wasmer.NewInstance(module, importObject)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
start, err := instance.Exports.GetWasiStartFunction()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
start()
functionName := r.URL.Query().Get("func")
if functionName == "" {
http.Error(w, "function name is required", http.StatusBadRequest)
return
}
// Gets the `sum` exported function from the WebAssembly instance.
sum, err := instance.Exports.GetFunction(functionName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
argValA := r.URL.Query().Get("a")
if argValA == "" {
http.Error(w, "arg a is required", http.StatusBadRequest)
return
}
a, err := strconv.Atoi(argValA)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
argValB := r.URL.Query().Get("b")
if argValB == "" {
http.Error(w, "arg b is required", http.StatusBadRequest)
return
}
b, err := strconv.Atoi(argValB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Invokes the `sum` function with the given arguments.
result, err := sum(a,b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// The code is executed, and the outcomes are memorized for
// compliance and verified
_, err = e.client.VerifiedSet(context.TODO(), []byte(fmt.Sprintf("%s-res", key)), []byte(fmt.Sprintf("%d", result)))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, fmt.Sprintf("verified WebAssembly app result: %d", result))
return
}
Now, we can run go run main.go
and visit the web page using a browser on port 8081.
http://<serverip>:8081/immutable/sum.wasm?func=Sum&a=5&b=7
As we see, the Wasm application has been served, immutably, from within immudb and so we can be absolutely certain that it hasn’t been tampered with and will always provide a client with the proper result in a deterministic fashion.
This is just one of the ideas our team has been experimenting with recently and it holds significant implications for running trusted applications over the web, and elsewhere. We’d like to hear your thoughts too. Join our discord and let us know what you think about this and everything else that immudb makes possible.