Run WASM at Ingress to Validate, Verify and Transform
Run WASM at Ingress to Validate, Verify and Transform

Introduction
When developer get tools they like, they tend to be more productive. An extensibility in a language of choice, provides the convenience of extensibility without learning a new language. WebAssembly runtime executes bytecode, generated by compiling code written in high level languages - javascript, C, C++, Python, Rust or Golang.
We explore WebAssembly support in EnRoute gateway, and how we can load bytecode generated from custom logic written in a language of choice. into the WebAssembly runtime using EnRoute.
We start with a goal to verify a request, validate it’s schema and transform the request. We use custom code that is compiled and loaded into the WebAssembly runtime, to achieve this
WASM support is a part of EnRoute Community Edition which is free to use without any enterprise license.
What this article covers
The following steps walk through what’s needed to work with a request
- Install EnRoute with WASM support
- Install example workload
- Program EnRoute to make the service externally available
- Create a WebAssembly filter
- Enable WebAssembly filter for service
- Ensure request MAC is validated and transform request to redact sensitive information
- Ensure JSON schema is validated
Next we go through the above mentioned steps in detail. We also look at how to build a WebAssembly image in compat variant which is then loaded into the Envoy WebAssembly runtime by EnRoute
Install EnRoute with WASM support
WASM support is provided in a separate EnRoute image. It can be installed using the following helm command after adding the helm repo and installing the enroute image with wasm
tag
helm repo add saaras https://getenroute.io
helm install enroute-demo saaras/enroute \
--set serviceAccount.create=true \
--create-namespace \
--namespace enroutedemo \
--set images.enrouteService.tag=wasm
Install example workload
kubectl create namespace httpbin
kubectl apply -f https://raw.githubusercontent.com/saarasio/enroute/master/helm-chart/httpbin-bin-service.yaml
Program EnRoute to make the service externally available
helm install httpbin-service-policy saaras/service-policy --set service.name=httpbin --set service.prefix=/post --set service.port=80 --namespace httpbin
Send request to External-IP of LoadBalancer
service to verify it works as expected. The getting started guide has details on setting this up.
curl -X POST -H 'Content-Type: application/json' -d '{}' 212.2.241.255/post
{
"args": {},
"data": "{}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Content-Length": "2",
"Content-Type": "application/json",
"Host": "212.2.241.255",
"User-Agent": "curl/7.68.0",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000",
"X-Envoy-External-Address": "192.168.1.5"
},
"json": {},
"origin": "152.70.114.65,192.168.1.5",
"url": "http://212.2.241.255/post"
}
Create a WebAssembly filter
cat <<EOF | kubectl apply -f -
apiVersion: enroute.saaras.io/v1
kind: HttpFilter
metadata:
name: httpbin-wasm-wasmfilter-oci
namespace: httpbin
spec:
httpFilterConfig:
config: |
{
"url" : "oci://saarasio/vvx-json"
}
name: httpbin-wasm-wasmfilter-oci
type: http_filter_wasm
EOF
Enable the WebAssembly filter for service
kubectl edit -n httpbin gatewayhosts.enroute.saaras.io httpbin-80-gatewayhost
5 apiVersion: enroute.saaras.io/v1
6 kind: GatewayHost
7 metadata:
8 annotations:
9 meta.helm.sh/release-name: httpbin-service-policy
10 meta.helm.sh/release-namespace: httpbin
11 creationTimestamp: "2022-04-26T14:56:48Z"
12 generation: 3
13 labels:
14 app: httpbin
15 app.kubernetes.io/managed-by: Helm
16 name: httpbin-80-gatewayhost
17 namespace: httpbin
18 resourceVersion: "3736"
19 uid: b2e39054-a959-40c3-9754-365b518b8ee6
20 spec:
21 routes:
22 - conditions:
23 - prefix: /post
24 filters:
25 - name: httpbin-80-rl2
26 type: route_filter_ratelimit
27 services:
28 - healthCheck:
29 healthyThresholdCount: 3
30 host: hc
31 intervalSeconds: 5
32 path: /
33 timeoutSeconds: 3
34 unhealthyThresholdCount: 3
35 name: httpbin
36 port: 80
37 virtualhost:
38 filters:
39 - name: httpbin-80-luatestfilter
40 type: http_filter_lua
41 - name: httpbin-wasm-wasmfilter-oci
42 type: http_filter_wasm
43 fqdn: '*'
Ensure MAC is verified and request is transformed with personal information redacted
We first create a payload JSON that we send to the server using a POST request. Here is the json payload in a file called payload.json
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2006-01-02 15:04",
"updated": "2011-01-19 22:15",
"dob": "2022-01-19 22:15"
},
"relationships": {
"author": {
"data": {"id": "42", "type": "people"}
}
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John",
"age": 80,
"gender": "male"
}
}
]
}
For verification, we calculate the SHA256 of the payload and compare it to the value we compute and send in the Digest header
cat payload.json | tr -d "\n" | openssl dgst -sha256 -binary | base64
h3wEb7jrjmwD8O7TDvOD3WGG23lfnzsfmODcbwLmdlk=
To send a request
curl -vv 212.2.241.255/post -H 'Content-Type: application/json' -H "Digest: sha256=h3wEb7jrjmwD8O7TDvOD3WGG23lfnzsfmODcbwLmdlk=" --data @/home/ubuntu/payload.json
{
"args": {},
"data": "{ \"data\": [{ \"type\": \"articles\", \"id\": \"1\", \"attributes\": { \"title\": \"JSON:API paints my bikeshed!\", \"body\": \"The shortest article. Ever.\", \"created\": \"2006-01-02 15:04\", \"updated\": \"2011-01-19 22:15\", \"dob\": \"0000-00-00 00:00\" }, \"relationships\": { \"author\": { \"data\": {\"id\": \"42\", \"type\": \"people\"} } } }], \"included\": [ { \"type\": \"people\", \"id\": \"42\", \"attributes\": { \"name\": \"John\", \"age\": 80, \"gender\": \"male\" } } ]}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Content-Length": "532",
"Content-Type": "application/json",
"Digest": "sha256=h3wEb7jrjmwD8O7TDvOD3WGG23lfnzsfmODcbwLmdlk=",
"Host": "212.2.241.255",
"User-Agent": "curl/7.68.0",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000",
"X-Envoy-External-Address": "192.168.1.5"
},
"json": {
"data": [
{
"attributes": {
"body": "The shortest article. Ever.",
"created": "2006-01-02 15:04",
"dob": "0000-00-00 00:00",
"title": "JSON:API paints my bikeshed!",
"updated": "2011-01-19 22:15"
},
"id": "1",
"relationships": {
"author": {
"data": {
"id": "42",
"type": "people"
}
}
},
"type": "articles"
}
],
"included": [
{
"attributes": {
"age": 80,
"gender": "male",
"name": "John"
},
"id": "42",
"type": "people"
}
]
},
"origin": "152.70.114.65,192.168.1.5",
"url": "http://212.2.241.255/post"
}
Ensure JSON body schema is validated
The JSON validation checks for presence of a few JSON elements in the payload and the format of certain fields. In the above payload, it checks for type
, id
, title
, body
and verifies the format of created
and updated
files to ensure valid dates are provided.
Code
Here is the code and it’s flow to achieve validate, verify, transform functions. The code can also be found on the saarasio github repo
Accumulate Request body and call validate, verify and transform functions
220 func (ctx *validateVerifyTransformContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
221 proxywasm.LogInfo("OnHttpRequestBody from Go!")
222 ctx.totalRequestBodySize += bodySize
223 if !endOfStream {
224 // Wait until we see the entire body to replace.
225 return types.ActionPause
226 }
227
228 jsonBody, err := proxywasm.GetHttpRequestBody(0, ctx.totalRequestBodySize)
229 if err != nil {
230 proxywasm.LogErrorf("failed to get request body: %v", err)
231 return types.ActionContinue
232 }
233
234 if ctx.digestValueRead {
235 secretKey := ""
236 vererr, received, computed := Verify(jsonBody, ctx.digestValue, secretKey)
237
238 proxywasm.LogInfof("WASM:OnHttpRequestBody() Received Digest: [%v] Computed Digest [%v]", received, computed)
239
240 if vererr != nil {
241 if err := proxywasm.SendHttpResponse(400, nil, []byte(vererr.Error()), -1); err != nil {
242 panic(err)
243 }
244 return types.ActionPause
245 }
246
247 if received != computed {
248 if err := proxywasm.SendHttpResponse(400, nil, []byte("Received digest doesn't match computed digest\n"), -1); err != nil {
249 panic(err)
250 }
251 return types.ActionPause
252 }
253 }
254
255 valerr := ValidateJSON(&jsonBody)
256
257 if valerr != nil {
258 if err := proxywasm.SendHttpResponse(400, nil, []byte(valerr.Error()), -1); err != nil {
259 panic(err)
260 }
261 return types.ActionPause
262
263 }
264
265 TransformJSON(&jsonBody)
266
267 return types.ActionContinue
268 }
Verify HMAC
103 //////// Verify Digest ////////
104
105 func Verify(bytesIn []byte, encodedHash string, secretKey string) (error, string, string) {
106 var valerr error
107
108 var payload string
109 var prefix string
110
111 if strings.HasPrefix(encodedHash, "sha1=") {
112 payload = strings.TrimPrefix(encodedHash, "sha1=")
113 prefix = "sha1"
114 } else if strings.HasPrefix(encodedHash, "sha256=") {
115 payload = strings.TrimPrefix(encodedHash, "sha256=")
116 prefix = "sha256"
117 } else {
118 valerr = fmt.Errorf("Unknown prefix in hash: [%v]\n", encodedHash)
119 return valerr, "", ""
120 }
121
122 messageMAC := payload
123
124 messageMACBuf2, _ := b64.StdEncoding.DecodeString(messageMAC)
125
126 var receivedHash, computedHash string
127
128 if prefix == "sha1" {
129 sum := sha1.Sum(bytesIn)
130 receivedHash = b64.StdEncoding.EncodeToString(messageMACBuf2[:])
131 computedHash = b64.StdEncoding.EncodeToString(sum[:])
132 } else if prefix == "sha256" {
133 sum := sha256.Sum256(bytesIn)
134 receivedHash = b64.StdEncoding.EncodeToString(messageMACBuf2[:])
135 computedHash = b64.StdEncoding.EncodeToString(sum[:])
136 } else {
137 valerr = fmt.Errorf("Unknown prefix in hash: [%v]\n", encodedHash)
138 }
139
140 return valerr, receivedHash, computedHash
141 }
Validate JSON
51 func ValidateJSON(jsonBody *[]byte) error {
52
53 // Verify elements are present
54 paths := [][]string{
55 []string{"data", "[0]", "type"},
56 []string{"data", "[0]", "id"},
57 []string{"data", "[0]", "attributes", "title"},
58 []string{"data", "[0]", "attributes", "body"},
59 []string{"data", "[0]", "attributes", "created"},
60 []string{"data", "[0]", "attributes", "updated"},
61 }
62 match := 0
63 var created, updated []byte
64 jsonparser.EachKey(*jsonBody, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
65 switch idx {
66 case 0: //
67 match += 1
68 case 1: //
69 match += 1
70 case 2: //
71 match += 1
72 case 3: //
73 match += 1
74 case 4: //
75 match += 1
76 created = value
77 case 5: //
78 match += 1
79 updated = value
80 }
81 }, paths...)
82
83 proxywasm.LogInfof("JSON Schema Validation Success, Match Count [%d]\n", match)
84
85 var ret error
86
87 // Verify created and updated are in date format
88 _, err := time.Parse(string(created), string(updated))
89 if err != nil {
90 proxywasm.LogError("Failed to parse created, updated")
91 ret = fmt.Errorf("Failed to parse created, updated")
92
93 }
94
95 if match != 6 {
96 ret = fmt.Errorf("Failed to validate json schema")
97 }
98
99 proxywasm.LogInfo("JSON Field Validation Success, Validated Created, Updated \n")
100 return ret
101 }
Transform JSON Body
143 //////// Transform JSON ////////
144
145 func TransformJSON(jsonBody *[]byte) {
146
147 paths := [][]string{
148 []string{"data", "[0]", "attributes", "dob"},
149 }
150 jsonparser.EachKey(*jsonBody, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
151 switch idx {
152 case 0: //
153 *jsonBody, _ = jsonparser.Set(*jsonBody, []byte("\"0000-00-00 00:00\""), "data", "[0]", "attributes", "dob")
154 proxywasm.ReplaceHttpRequestBody(*jsonBody)
155 }
156 }, paths...)
157
158 }
Conclusion
WebAssembly provides a flexible extension mechanism to customize request verification, validation and transformation. WebAssembly in a proxy implements the proxy-wasm abi spec, that is implemented by SDKs. The SDKs in different languages provide a mechanism to interface with WebAssembly runtime. We use the go sdk that implements the proxy-wasm abi spec to achieve this.

The code generated in the above step is compiled to WASM using the WASI libc when compiling using tinygo The bytecode generated after the compilation is packaged into a WASM artifact in compat variant that is then loaded into the Envoy WebAssembly runtime.
In the next article we cover how to build a compat variant of an image that can be loaded by EnRoute and details on proxy-wasm ABI that let’s us recieve callbacks for request processing.