Skip to content

Commit af0fc36

Browse files
authored
Merge pull request #38 from maritimeconnectivity/wasm
Use Go and WASM for cryptographic operations
2 parents 1a68aa5 + 318ca07 commit af0fc36

File tree

16 files changed

+7358
-6041
lines changed

16 files changed

+7358
-6041
lines changed

.github/workflows/build.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Build project
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: pnpm/action-setup@v4
15+
with:
16+
version: 9
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: 20.x
21+
cache: 'pnpm'
22+
23+
- uses: actions/setup-go@v5
24+
with:
25+
go-version: 1.24
26+
cache-dependency-path: ./go/go.sum
27+
28+
- name: build
29+
run: |
30+
pnpm install
31+
pnpm run build:go
32+
pnpm run build
Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
name: NodeJS with Webpack
1+
name: Deploy with Webpack
22

33
on:
44
push:
55
branches: [ "main" ]
6-
pull_request:
7-
branches: [ "main" ]
86

97
jobs:
108
build:
119
runs-on: ubuntu-latest
1210

1311
steps:
14-
- uses: actions/checkout@v4
12+
- uses: actions/checkout@v4
13+
14+
- uses: pnpm/action-setup@v4
15+
with:
16+
version: 9
17+
18+
- name: Use Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: 20.x
22+
cache: 'pnpm'
1523

16-
- uses: pnpm/action-setup@v4
17-
with:
18-
version: 9
19-
20-
- name: Use Node.js
21-
uses: actions/setup-node@v4
22-
with:
23-
node-version: 18.x
24-
cache: 'pnpm'
24+
- name: Setup Go
25+
uses: actions/setup-go@v5
26+
with:
27+
go-version: 1.24
28+
cache-dependency-path: ./go/go.sum
2529

26-
- name: Build
27-
env:
28-
GITHUB_TOKEN: ${{ secrets.PAT }}
29-
run: |
30-
pnpm install
31-
pnpm install -g @angular/cli
32-
git config --global user.name "GhPages Deploy Bot"
33-
git config --global user.email "[email protected]"
34-
ng deploy -c test --repo=https://github.com/maritimeconnectivity/test-management-portal-pages --cname=test-management.maritimeconnectivity.net --dir="dist/management-portal-clr"
30+
- name: Build
31+
env:
32+
GITHUB_TOKEN: ${{ secrets.PAT }}
33+
run: |
34+
pnpm install
35+
pnpm install -g @angular/cli
36+
git config --global user.name "GhPages Deploy Bot"
37+
git config --global user.email "[email protected]"
38+
pnpm run build:go
39+
ng deploy -c test --repo=https://github.com/maritimeconnectivity/test-management-portal-pages --cname=test-management.maritimeconnectivity.net --dir="dist/management-portal-clr"

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
1111
You can experience a live demo from [our public demonstrator environment](https://management.maritimeconnectivity.net).
1212

1313
# Development
14-
## Requirement
15-
- node v20.17.0
16-
- pnpm v8.15.4
14+
## Requirements
15+
- node v20.17.0+
16+
- pnpm v9.7.0+
17+
- Go 1.24+
18+
19+
## Building the Go WASM module
20+
The Go WASM module, which is used for generating public/private key-pairs, certificate signing request and PKCS#12 keystores, can be built by running `pnpm run build:go`.
21+
22+
This needs to be done before performing any of the following actions.
1723

1824
## Development server
1925

go/go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module webcrypto-test
2+
3+
go 1.24.1
4+
5+
require software.sslmate.com/src/go-pkcs12 v0.5.0
6+
7+
require golang.org/x/crypto v0.36.0 // indirect

go/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
2+
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
3+
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
4+
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

go/main.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Copyright (c) 2025 Maritime Connectivity Platform Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"crypto/ecdsa"
21+
"crypto/elliptic"
22+
"crypto/rand"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
26+
"software.sslmate.com/src/go-pkcs12"
27+
"syscall/js"
28+
)
29+
30+
func createCsrWrapper() js.Func {
31+
return js.FuncOf(func(this js.Value, args []js.Value) any {
32+
promiseHandler := js.FuncOf(func(this js.Value, args []js.Value) any {
33+
resolve := args[0]
34+
reject := args[1]
35+
errorConstructor := js.Global().Get("Error")
36+
37+
go func() {
38+
privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
39+
if err != nil {
40+
errorObject := errorConstructor.New("Failed to generate key")
41+
reject.Invoke(errorObject)
42+
return
43+
}
44+
45+
pkcs8Key, err := x509.MarshalPKCS8PrivateKey(privKey)
46+
if err != nil {
47+
errorObject := errorConstructor.New(err.Error())
48+
reject.Invoke(errorObject)
49+
return
50+
}
51+
52+
subject := pkix.Name{
53+
CommonName: "Name",
54+
}
55+
56+
template := &x509.CertificateRequest{
57+
Subject: subject,
58+
SignatureAlgorithm: x509.ECDSAWithSHA384,
59+
}
60+
61+
csr, err := x509.CreateCertificateRequest(rand.Reader, template, privKey)
62+
if err != nil {
63+
errorObject := errorConstructor.New("Failed to create CSR: " + err.Error())
64+
reject.Invoke(errorObject)
65+
return
66+
}
67+
68+
// PEM encode CSR
69+
block := &pem.Block{
70+
Type: "CERTIFICATE REQUEST",
71+
Bytes: csr,
72+
}
73+
pemBytes := pem.EncodeToMemory(block)
74+
if pemBytes == nil {
75+
errorObject := errorConstructor.New("Failed to PEM encode certificate request")
76+
reject.Invoke(errorObject)
77+
return
78+
}
79+
pemCsr := string(pemBytes)
80+
81+
// PEM encode private key
82+
block = &pem.Block{
83+
Type: "PRIVATE KEY",
84+
Bytes: pkcs8Key,
85+
}
86+
pemBytes = pem.EncodeToMemory(block)
87+
if pemBytes == nil {
88+
errorObject := errorConstructor.New("Failed to PEM encode private key")
89+
reject.Invoke(errorObject)
90+
return
91+
}
92+
pemPrivKey := string(pemBytes)
93+
94+
// Extract public key
95+
pubKey := privKey.Public()
96+
rawPubKey, err := x509.MarshalPKIXPublicKey(pubKey.(*ecdsa.PublicKey))
97+
if err != nil {
98+
errorObject := errorConstructor.New(err.Error())
99+
reject.Invoke(errorObject)
100+
return
101+
}
102+
103+
// PEM encode public key
104+
block = &pem.Block{
105+
Type: "PUBLIC KEY",
106+
Bytes: rawPubKey,
107+
}
108+
pemBytes = pem.EncodeToMemory(block)
109+
if pemBytes == nil {
110+
errorObject := errorConstructor.New("Failed to PEM encode public key")
111+
reject.Invoke(errorObject)
112+
return
113+
}
114+
pemPubKey := string(pemBytes)
115+
116+
// Copy the generated private key to a JS Uint8Array
117+
jsBytes := js.Global().Get("Uint8Array").New(len(pkcs8Key))
118+
js.CopyBytesToJS(jsBytes, pkcs8Key)
119+
120+
ret := map[string]any{
121+
"privateKeyPem": pemPrivKey,
122+
"rawPrivateKey": jsBytes,
123+
"publicKeyPem": pemPubKey,
124+
"csr": pemCsr,
125+
}
126+
127+
resolve.Invoke(js.ValueOf(ret))
128+
}()
129+
130+
return nil
131+
})
132+
133+
return js.Global().Get("Promise").New(promiseHandler)
134+
})
135+
}
136+
137+
func createPKCS12KeystoreWrapper() js.Func {
138+
return js.FuncOf(func(this js.Value, args []js.Value) any {
139+
if len(args) != 2 {
140+
return "Invalid number of args"
141+
}
142+
certs := args[0].String()
143+
rawPrivateKey := args[1]
144+
145+
promiseHandler := js.FuncOf(func(this js.Value, args []js.Value) any {
146+
resolve := args[0]
147+
reject := args[1]
148+
errorConstructor := js.Global().Get("Error")
149+
150+
go func() {
151+
rawPrivateKeyBytes := make([]byte, rawPrivateKey.Get("length").Int())
152+
js.CopyBytesToGo(rawPrivateKeyBytes, rawPrivateKey)
153+
154+
var cert *x509.Certificate
155+
var caCert *x509.Certificate
156+
157+
// PEM decode the first certificate
158+
block, rest := pem.Decode([]byte(certs))
159+
if block == nil {
160+
errorObject := errorConstructor.New("Failed to PEM decode certificate")
161+
reject.Invoke(errorObject)
162+
return
163+
}
164+
cert, err := x509.ParseCertificate(block.Bytes)
165+
if err != nil {
166+
errorObject := errorConstructor.New("Failed to parse certificate: " + err.Error())
167+
reject.Invoke(errorObject)
168+
return
169+
}
170+
171+
// If there are still more left to decode, we decode the rest as the intermediate CA certificate
172+
if len(rest) > 0 {
173+
block, _ = pem.Decode(rest)
174+
if block == nil {
175+
errorObject := errorConstructor.New("Failed to PEM decode CA certificate")
176+
reject.Invoke(errorObject)
177+
return
178+
}
179+
caCert, err = x509.ParseCertificate(block.Bytes)
180+
if err != nil {
181+
errorObject := errorConstructor.New("Failed to parse CA certificate: " + err.Error())
182+
reject.Invoke(errorObject)
183+
return
184+
}
185+
}
186+
187+
privKey, err := x509.ParsePKCS8PrivateKey(rawPrivateKeyBytes)
188+
189+
// Generate random 26 character password
190+
password := rand.Text()
191+
192+
// Build the PKCS#12 keystore
193+
pfx, err := pkcs12.Modern.Encode(privKey, cert, []*x509.Certificate{caCert}, password)
194+
if err != nil {
195+
errorObject := errorConstructor.New("Failed to encode PKCS12 keystore")
196+
reject.Invoke(errorObject)
197+
return
198+
}
199+
200+
// Copy the generated keystore to a JS Uint8Array
201+
jsBytes := js.Global().Get("Uint8Array").New(len(pfx))
202+
js.CopyBytesToJS(jsBytes, pfx)
203+
204+
ret := map[string]any{
205+
"keystore": jsBytes,
206+
"password": password,
207+
}
208+
209+
resolve.Invoke(js.ValueOf(ret))
210+
}()
211+
212+
return nil
213+
})
214+
215+
return js.Global().Get("Promise").New(promiseHandler)
216+
})
217+
}
218+
219+
func main() {
220+
js.Global().Set("createCsr", createCsrWrapper())
221+
js.Global().Set("createPKCS12Keystore", createPKCS12KeystoreWrapper())
222+
<-make(chan bool)
223+
}

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"ng": "ng",
1414
"start": "ng serve",
1515
"build": "ng build",
16+
"build:go": "cd go && GOOS=js GOARCH=wasm go build -o ../src/assets/wasm/main.wasm && cp $(go env GOROOT)/lib/wasm/wasm_exec.js ../src/assets/js/wasm_exec.js",
1617
"watch": "ng build --watch --configuration development",
1718
"test": "ng test",
1819
"lint": "ng lint"
@@ -50,13 +51,9 @@
5051
"leaflet": "^1.9.4",
5152
"leaflet-draw": "^1.0.2",
5253
"lucene-query-string-builder": "^1.0.8",
53-
"pkijs": "^3.2.4",
54-
"pvtsutils": "^1.3.5",
55-
"pvutils": "^1.1.3",
5654
"rxjs": "~7.8.0",
5755
"shortid": "^2.2.16",
5856
"tslib": "^2.3.0",
59-
"wkt": "link:@types/@terraformer/wkt",
6057
"zone.js": "^0.14.10"
6158
},
6259
"devDependencies": {
@@ -67,6 +64,7 @@
6764
"@types/d3-shape": "^3.1.6",
6865
"@types/file-saver": "^2.0.7",
6966
"@types/geojson": "^7946.0.14",
67+
"@types/golang-wasm-exec": "^1.15.2",
7068
"@types/jasmine": "~4.3.0",
7169
"@types/leaflet": "^1.9.14",
7270
"@types/leaflet-draw": "^1.0.11",

0 commit comments

Comments
 (0)