Skip to content

Commit

Permalink
Simplify the PKI client-side code and remove the service worker. (#168)
Browse files Browse the repository at this point in the history
### Description

Simplify the PKI client-side code and remove the service worker.

### Type of change

* [ ] New feature
* [x] Feature improvement
* [ ] Bug fix
* [ ] Documentation
* [ ] Cleanup / refactoring
* [ ] Other (please explain)

### How is this change tested ?

* [x] Unit tests
* [x] Manual tests (explain)
* [ ] Tests are not needed
  • Loading branch information
rthellend authored Dec 12, 2024
1 parent 0833162 commit c832379
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 239 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# TLSPROXY Release Notes

## next

### :wrench: Misc

* Simplify the PKI client-side code and remove the service worker.

## v0.14.0

### :star2: New feature
Expand Down
13 changes: 9 additions & 4 deletions proxy/internal/pki/certs.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=10, minimum-scale=0.1" />
<link rel="stylesheet" type="text/css" href="/.sso/style.css" />
<link rel="stylesheet" type="text/css" href="?get=static&file=style.css" />
<script src="?get=static&file=wasm_exec.js"></script>
<script src="?get=static&file=certs.js"></script>
</head>
<body>
Expand Down Expand Up @@ -102,11 +103,15 @@ <h2>Option 1</h2>
<option value="rsa-4096">RSA 4096</option>
</select></div>
<div><b>Label:</b></div><div><input type="text" name="label" size="20" placeholder="optional common name suffix" /></div>
<div><b>DNS Name:</b></div><div><input type="text" name="dnsname" size="20" placeholder="optional (server certs)" /></div>
<div><b>Password:</b></div><div><input type="password" name="pw1" size="20" placeholder="required" /></div>
<div><b>Re-type Password:</b></div><div><input type="password" name="pw2" size="20" placeholder="required" /></div>
<div><b>Usage:</b></div><div><select name="usage" onchange="selectUsage(this)">
<option value="client">Client</option>
<option value="server">Server</select>
</div>
<button onclick="generateKeyAndCert(this.form);">Get Key &amp; Cert</button>
<div class="dnsinput"><b>DNS Name:</b></div><div class="dnsinput"><input type="text" name="dnsname" size="20" placeholder="required" /></div>
<div><b>Password:</b></div><div><input type="password" name="pw1" size="20" placeholder="required" autocomplete="new-password" /></div>
<div><b>Re-type Password:</b></div><div><input type="password" name="pw2" size="20" placeholder="required" autocomplete="new-password" /></div>
</div>
<button onclick="generateKeyAndCert(this);">Get Key &amp; Cert</button>
<button onclick="hideForm();">Cancel</button>
</form>
<hr />
Expand Down
95 changes: 52 additions & 43 deletions proxy/internal/pki/certs.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

'use strict';

window.pkiApp = {};
pkiApp.ready = new Promise(resolve => {
pkiApp.pkiwasmIsReady = () => {
console.log('PKI WASM is ready');
resolve();
};
});

const go = new Go();
let wasmLoaded = false;

function requestCert(csr) {
fetch('?get=requestCert', {
method: 'POST',
Expand Down Expand Up @@ -85,13 +98,25 @@ function downloadCert(sn) {
window.location = '?get=downloadCert&sn='+encodeURIComponent(sn);
}
function showForm() {
if (!wasmLoaded) {
WebAssembly.instantiateStreaming(fetch('?get=static&file=pki.wasm.bz2'), go.importObject).then(r => go.run(r.instance));
wasmLoaded = true;
}
document.getElementById('csrform').style.display = 'block';
}
function hideForm() {
document.getElementById('csrform').style.display = 'none';
window.location.reload();
}
function generateKeyAndCert(f) {
function selectUsage(e) {
const isServer = e.options[e.selectedIndex].value === 'server';
for (const e of document.querySelectorAll('.dnsinput')) {
if (isServer) e.classList.add('selected');
else e.classList.remove('selected');
}
}
function generateKeyAndCert(b) {
let f = b.form;
if (f.pw1.value.length < 6) {
alert('Password must be at least 6 characters');
return;
Expand All @@ -100,52 +125,36 @@ function generateKeyAndCert(f) {
alert('Passwords don\'t match');
return;
}
if (f.usage.options[f.usage.selectedIndex].value === 'server' && f.dnsname.value === '') {
alert('DNS Name is required');
return;
}
const pw = f.pw1.value;
const fmt = f.format.value;
const kty = f.keytype.value;
const label = f.label.value;
const dns = f.dnsname.value;
f.pw1.value = '';
f.pw2.value = '';
const path = window.location.pathname + '/generateKeyAndCert';
navigator.serviceWorker.register('?get=static&file=sw.js', {scope: path})
.then(r => r.update())
.then(r => {
console.log('Service worker ready');
document.getElementById('csrform').style.display = 'none';
const f = document.createElement('form');
f.setAttribute('method', 'post');
f.setAttribute('action', path);
let p = document.createElement('input');
p.setAttribute('type', 'hidden');
p.setAttribute('name', 'password');
p.setAttribute('value', pw);
f.appendChild(p);
p = document.createElement('input');
p.setAttribute('type', 'hidden');
p.setAttribute('name', 'format');
p.setAttribute('value', fmt);
f.appendChild(p);
p = document.createElement('input');
p.setAttribute('type', 'hidden');
p.setAttribute('name', 'keytype');
p.setAttribute('value', kty);
f.appendChild(p);
p = document.createElement('input');
p.setAttribute('type', 'hidden');
p.setAttribute('name', 'label');
p.setAttribute('value', label);
f.appendChild(p);
p = document.createElement('input');
p.setAttribute('type', 'hidden');
p.setAttribute('name', 'dnsname');
p.setAttribute('value', dns);
f.appendChild(p);
document.body.appendChild(f);
f.submit();
})
.catch(err => console.error('Service worker update failed', err))

const oldb = b.textContent;
b.disabled = true;
b.textContent = 'working...';
document.body.classList.add('waiting');
pkiApp.ready
.then(() => pkiApp.getCertificate({
'keytype': f.keytype.value,
'format': f.format.value,
'password': pw,
'label': f.label.value,
'dnsname': f.dnsname.value,
}))
.then(() => window.location.reload())
.catch(err => {
b.disabled = false;
b.textContent = oldb;
document.body.classList.remove('waiting');
console.error('getCertificate failed', err);
alert('Request failed: '+err.message);
});
}

function showView(sn) {
fetch('?get=downloadCert&sn='+encodeURIComponent(sn))
.then(resp => {
Expand Down
70 changes: 39 additions & 31 deletions proxy/internal/pki/clientwasm/impl/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ package impl

import (
"bytes"
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"strings"

"golang.org/x/crypto/openpgp"
Expand All @@ -41,30 +43,40 @@ import (
"github.com/c2FmZQ/tlsproxy/proxy/internal/pki/keys"
)

var (
privateKeys map[int]keyData
)

type keyData struct {
key crypto.PrivateKey
format string
password string
}

func MakeCSR(id int, keyType, format, label, dnsname, password string) ([]byte, error) {
func GetCertificate(url, keyType, format, label, dnsname, password string) (data []byte, contentType, filename string, err error) {
privKey, err := keys.GenerateKey(keyType)
if err != nil {
return nil, err
return nil, "", "", err
}
csr, err := makeCSR(privKey, label, dnsname)
if err != nil {
return nil, "", "", err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(csr))
if err != nil {
return nil, "", "", err
}
req.Header.Set("content-type", "application/x-pem-file")
req.Header.Set("x-csrf-check", "1")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", "", err
}
defer resp.Body.Close()
var reqResult struct {
Cert string `json:"cert"`
Result string `json:"result"`
}
if privateKeys == nil {
privateKeys = make(map[int]keyData)
if err := json.NewDecoder(&io.LimitedReader{R: resp.Body, N: 102400}).Decode(&reqResult); err != nil {
return nil, "", "", err
}
privateKeys[id] = keyData{
key: privKey,
format: format,
password: password,
if reqResult.Result != "ok" {
return nil, "", "", fmt.Errorf("result: %s", reqResult.Result)
}
return makeResponse(privKey, []byte(reqResult.Cert), format, password)
}

func makeCSR(privKey any, label, dnsname string) ([]byte, error) {
templ := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: label},
}
Expand All @@ -78,29 +90,25 @@ func MakeCSR(id int, keyType, format, label, dnsname, password string) ([]byte,
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: raw}), nil
}

func MakeResponse(id int, pemCert string) ([]byte, string, string, error) {
block, _ := pem.Decode([]byte(pemCert))
func makeResponse(privKey any, pemCert []byte, format, password string) ([]byte, string, string, error) {
block, _ := pem.Decode(pemCert)
if block == nil || block.Type != "CERTIFICATE" {
return nil, "", "", errors.New("invalid pem certificate")
return nil, "", "", fmt.Errorf("invalid pem certificate: %q", pemCert)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, "", "", errors.New("invalid certificate")
}
sn := hex.EncodeToString(cert.SerialNumber.Bytes())
kd, ok := privateKeys[id]
if !ok {
return nil, "", "", errors.New("invalid id")
}
delete(privateKeys, id)
switch kd.format {

switch format {
case "gpg":
b, err := x509.MarshalPKCS8PrivateKey(kd.key)
b, err := x509.MarshalPKCS8PrivateKey(privKey)
if err != nil {
return nil, "", "", fmt.Errorf("x509.MarshalPKCS8PrivateKey: %v\n", err)
}
var buf bytes.Buffer
w, err := openpgp.SymmetricallyEncrypt(&buf, []byte(kd.password), &openpgp.FileHints{FileName: sn + ".pem", ModTime: cert.NotBefore}, nil)
w, err := openpgp.SymmetricallyEncrypt(&buf, []byte(password), &openpgp.FileHints{FileName: sn + ".pem", ModTime: cert.NotBefore}, nil)
if err != nil {
return nil, "", "", fmt.Errorf("openpgp.SymmetricallyEncrypt: %v\n", err)
}
Expand All @@ -117,7 +125,7 @@ func MakeResponse(id int, pemCert string) ([]byte, string, string, error) {

case "p12":
enc := pkcs12.Modern.WithIterations(250000)
p12, err := enc.Encode(kd.key, cert, nil, kd.password)
p12, err := enc.Encode(privKey, cert, nil, password)
if err != nil {
return nil, "", "", fmt.Errorf("pkcs12.Encode: %v", err)
}
Expand Down
19 changes: 12 additions & 7 deletions proxy/internal/pki/clientwasm/impl/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ import (
"math/big"
"testing"
"time"

"github.com/c2FmZQ/tlsproxy/proxy/internal/pki/keys"
)

func TestKeyTypeFormat(t *testing.T) {
count := 0
for _, tc := range []struct {
keyType string
format string
Expand All @@ -50,10 +51,14 @@ func TestKeyTypeFormat(t *testing.T) {
{keyType: "rsa-2048", format: "p12", contentType: "application/x-pkcs12", ext: ".p12"},
{keyType: "ed25519", format: "gpg", dnsName: "example.com", contentType: "application/octet-stream", ext: ".pem.gpg"},
} {
count++
csrPEM, err := MakeCSR(count, tc.keyType, tc.format, tc.label, tc.dnsName, "foo")
privKey, err := keys.GenerateKey(tc.keyType)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}

csrPEM, err := makeCSR(privKey, tc.label, tc.dnsName)
if err != nil {
t.Fatalf("MakeCSR: %v", err)
t.Fatalf("makeCSR: %v", err)
}
block, _ := pem.Decode(csrPEM)
if block == nil {
Expand All @@ -68,7 +73,7 @@ func TestKeyTypeFormat(t *testing.T) {
t.Fatalf("x509.ParseCertificateRequest: %v", err)
}

_, privKey, err := ed25519.GenerateKey(rand.Reader)
_, caKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("ed25519.GenerateKey: %v", err)
}
Expand All @@ -86,13 +91,13 @@ func TestKeyTypeFormat(t *testing.T) {
KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
raw, err := x509.CreateCertificate(rand.Reader, templ, templ, cr.PublicKey, privKey)
raw, err := x509.CreateCertificate(rand.Reader, templ, templ, cr.PublicKey, caKey)
if err != nil {
t.Fatalf("x509.CreateCertificate: %v", err)
}
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: raw})

_, contentType, fileName, err := MakeResponse(count, string(cert))
_, contentType, fileName, err := makeResponse(privKey, cert, tc.format, "foo")
if err != nil {
t.Fatalf("MakeResponse: %v", err)
}
Expand Down
Loading

0 comments on commit c832379

Please sign in to comment.