Add web UI for SSH CA, Transit, and User engines; full security audit and remediation

Web UI: Added browser-based management for all three remaining engines
(SSH CA, Transit, User E2E). Includes gRPC client wiring, handler files,
7 HTML templates, dashboard mount forms, and conditional navigation links.
Fixed REST API routes to match design specs (SSH CA cert singular paths,
Transit PATCH for update-key-config).

Security audit: Conducted full-system audit covering crypto core, all
engine implementations, API servers, policy engine, auth, deployment,
and documentation. Identified 42 new findings (#39-#80) across all
severity levels.

Remediation of all 8 High findings:
- #68: Replaced 14 JSON-injection-vulnerable error responses with safe
  json.Encoder via writeJSONError helper
- #48: Added two-layer path traversal defense (barrier validatePath
  rejects ".." segments; engine ValidateName enforces safe name pattern)
- #39: Extended RLock through entire crypto operations in barrier
  Get/Put/Delete/List to eliminate TOCTOU race with Seal
- #40: Unified ReWrapKeys and seal_config UPDATE into single SQLite
  transaction to prevent irrecoverable data loss on crash during MEK
  rotation
- #49: Added resolveTTL to CA engine enforcing issuer MaxTTL ceiling
  on handleIssue and handleSignCSR
- #61: Store raw ECDH private key bytes in userState for effective
  zeroization on Seal
- #62: Fixed user engine policy resource path from mountPath to
  mountName() so policy rules match correctly
- #69: Added newPolicyChecker helper and passed service-level policy
  evaluation to all 25 typed REST handler engine.Request structs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 22:02:06 -07:00
parent 128f5abc4d
commit a80323e320
29 changed files with 5061 additions and 647 deletions

View File

@@ -25,6 +25,12 @@
<td>
{{if eq (printf "%s" .Type) "ca"}}
<a href="/pki">{{.Name}}</a>
{{else if eq (printf "%s" .Type) "sshca"}}
<a href="/sshca">{{.Name}}</a>
{{else if eq (printf "%s" .Type) "transit"}}
<a href="/transit">{{.Name}}</a>
{{else if eq (printf "%s" .Type) "user"}}
<a href="/user">{{.Name}}</a>
{{else}}
{{.Name}}
{{end}}
@@ -75,6 +81,68 @@
</div>
</form>
</details>
<details>
<summary>Mount an SSH CA engine</summary>
<form method="post" action="/dashboard/mount-engine">
{{csrfField}}
<input type="hidden" name="type" value="sshca">
<div class="form-row">
<div class="form-group">
<label for="sshca_name">Mount Name</label>
<input type="text" id="sshca_name" name="name" placeholder="sshca" required>
</div>
<div class="form-group">
<label for="sshca_algo">Key Algorithm</label>
<select id="sshca_algo" name="key_algorithm">
<option value="ed25519">ed25519 (default)</option>
<option value="ecdsa-p256">ecdsa-p256</option>
<option value="ecdsa-p384">ecdsa-p384</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit">Mount</button>
</div>
</form>
</details>
<details>
<summary>Mount a Transit engine</summary>
<form method="post" action="/dashboard/mount-engine">
{{csrfField}}
<input type="hidden" name="type" value="transit">
<div class="form-group">
<label for="transit_name">Mount Name</label>
<input type="text" id="transit_name" name="name" placeholder="transit" required>
</div>
<div class="form-actions">
<button type="submit">Mount</button>
</div>
</form>
</details>
<details>
<summary>Mount a User Crypto engine</summary>
<form method="post" action="/dashboard/mount-engine">
{{csrfField}}
<input type="hidden" name="type" value="user">
<div class="form-row">
<div class="form-group">
<label for="user_name">Mount Name</label>
<input type="text" id="user_name" name="name" placeholder="user" required>
</div>
<div class="form-group">
<label for="user_algo">Key Algorithm</label>
<select id="user_algo" name="key_algorithm">
<option value="x25519">x25519 (default)</option>
<option value="ecdh-p256">ecdh-p256</option>
<option value="ecdh-p384">ecdh-p384</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit">Mount</button>
</div>
</form>
</details>
</div>
<div class="card">

View File

@@ -14,6 +14,9 @@
{{if .Username}}
<a href="/dashboard" class="btn btn-ghost btn-sm">Dashboard</a>
<a href="/pki" class="btn btn-ghost btn-sm">PKI</a>
{{if .HasSSHCA}}<a href="/sshca" class="btn btn-ghost btn-sm">SSH CA</a>{{end}}
{{if .HasTransit}}<a href="/transit" class="btn btn-ghost btn-sm">Transit</a>{{end}}
{{if .HasUser}}<a href="/user" class="btn btn-ghost btn-sm">User Crypto</a>{{end}}
{{if .IsAdmin}}<a href="/policy" class="btn btn-ghost btn-sm">Policy</a>{{end}}
<span class="topnav-user">{{.Username}}</span>
{{if .IsAdmin}}<span class="badge">admin</span>{{end}}

216
web/templates/sshca.html Normal file
View File

@@ -0,0 +1,216 @@
{{define "title"}} - SSH CA: {{.MountName}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>SSH CA: {{.MountName}}</h2>
<div class="page-meta">
<a href="/dashboard">&larr; Dashboard</a>
</div>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<div class="card">
<div class="card-title">CA Public Key</div>
{{if .CAPublicKey}}
<textarea rows="3" class="pem-input" readonly>{{.CAPublicKey}}</textarea>
<p style="margin-top: 0.5rem; margin-bottom: 0;">
<a href="/v1/sshca/{{.MountName}}/ca" download="ca.pub">Download CA Public Key</a>
</p>
{{else}}
<p>CA public key not available.</p>
{{end}}
</div>
<div class="card">
<div class="card-title">Sign User Certificate</div>
{{if .SignUserResult}}
<div class="success">
<p>User certificate signed. Serial: <code>{{.SignUserResult.Serial}}</code> &mdash; Expires: {{.SignUserResult.ExpiresAt}}</p>
<div class="form-group">
<label>Certificate</label>
<textarea rows="6" class="pem-input" readonly>{{.SignUserResult.CertData}}</textarea>
</div>
</div>
{{else}}
<form method="post" action="/sshca/sign-user">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="sign_user_profile">Profile</label>
<select id="sign_user_profile" name="profile">
<option value="">(default)</option>
{{range .Profiles}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="sign_user_ttl">TTL <small style="text-transform:none;letter-spacing:0;">(optional)</small></label>
<input type="text" id="sign_user_ttl" name="ttl" placeholder="24h">
</div>
</div>
<div class="form-group">
<label for="sign_user_key">Public Key <small style="text-transform:none;letter-spacing:0;">(authorized_keys format)</small></label>
<textarea id="sign_user_key" name="public_key" rows="3" class="pem-input" placeholder="ssh-ed25519 AAAA..." required></textarea>
</div>
<div class="form-group">
<label for="sign_user_principals">Principals <small style="text-transform:none;letter-spacing:0;">(one per line, blank = your username)</small></label>
<textarea id="sign_user_principals" name="principals" rows="2" placeholder="kyle"></textarea>
</div>
<div class="form-actions">
<button type="submit">Sign User Certificate</button>
</div>
</form>
{{end}}
</div>
<div class="card">
<div class="card-title">Sign Host Certificate</div>
{{if .SignHostResult}}
<div class="success">
<p>Host certificate signed. Serial: <code>{{.SignHostResult.Serial}}</code> &mdash; Expires: {{.SignHostResult.ExpiresAt}}</p>
<div class="form-group">
<label>Certificate</label>
<textarea rows="6" class="pem-input" readonly>{{.SignHostResult.CertData}}</textarea>
</div>
</div>
{{else}}
<form method="post" action="/sshca/sign-host">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="sign_host_hostname">Hostname</label>
<input type="text" id="sign_host_hostname" name="hostname" placeholder="server.example.com" required>
</div>
<div class="form-group">
<label for="sign_host_ttl">TTL <small style="text-transform:none;letter-spacing:0;">(optional)</small></label>
<input type="text" id="sign_host_ttl" name="ttl" placeholder="720h">
</div>
</div>
<div class="form-group">
<label for="sign_host_key">Public Key <small style="text-transform:none;letter-spacing:0;">(authorized_keys format)</small></label>
<textarea id="sign_host_key" name="public_key" rows="3" class="pem-input" placeholder="ssh-ed25519 AAAA..." required></textarea>
</div>
<div class="form-actions">
<button type="submit">Sign Host Certificate</button>
</div>
</form>
{{end}}
</div>
<div class="card">
<div class="card-title">Signing Profiles</div>
{{if .Profiles}}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Profiles}}
<tr>
<td><a href="/sshca/profile/{{.Name}}">{{.Name}}</a></td>
<td><a href="/sshca/profile/{{.Name}}">View</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No signing profiles configured.</p>
{{end}}
{{if .IsAdmin}}
<details style="margin-top: 1rem;">
<summary>Create Profile</summary>
<form method="post" action="/sshca/profile/create">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="profile_name">Name</label>
<input type="text" id="profile_name" name="name" placeholder="engineering" required>
</div>
<div class="form-group">
<label for="profile_max_ttl">Max TTL</label>
<input type="text" id="profile_max_ttl" name="max_ttl" placeholder="8760h">
</div>
</div>
<div class="form-group">
<label for="profile_principals">Allowed Principals <small style="text-transform:none;letter-spacing:0;">(one per line, blank = any)</small></label>
<textarea id="profile_principals" name="allowed_principals" rows="2" placeholder="*"></textarea>
</div>
<div class="form-group">
<label>Extensions</label>
<div class="checkbox-group">
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-pty" checked> permit-pty</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-agent-forwarding"> permit-agent-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-port-forwarding"> permit-port-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-X11-forwarding"> permit-X11-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-user-rc"> permit-user-rc</label>
</div>
</div>
<details>
<summary>Critical Options</summary>
<div class="form-row">
<div class="form-group">
<label for="profile_force_cmd">Force Command</label>
<input type="text" id="profile_force_cmd" name="force_command" placeholder="(none)">
</div>
<div class="form-group">
<label for="profile_source_addr">Source Address</label>
<input type="text" id="profile_source_addr" name="source_address" placeholder="(none)">
</div>
</div>
</details>
<div class="form-actions">
<button type="submit">Create Profile</button>
</div>
</form>
</details>
{{end}}
</div>
<div class="card">
<div class="card-title">Certificates</div>
{{if .Certs}}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Serial</th>
<th>Type</th>
<th>Key ID</th>
<th>Principals</th>
<th>Issued By</th>
<th>Expires</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Certs}}
<tr>
<td><a href="/sshca/cert/{{.Serial}}"><code>{{.Serial}}</code></a></td>
<td>{{.CertType}}</td>
<td>{{.KeyID}}</td>
<td>{{.Principals}}</td>
<td>{{.IssuedBy}}</td>
<td>{{.ExpiresAt}}</td>
<td>{{if .Revoked}}<span class="badge badge-danger">revoked</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No certificates issued.</p>
{{end}}
</div>
{{if .IsAdmin}}
<div class="card">
<div class="card-title">Key Revocation List</div>
<p><a href="/v1/sshca/{{.MountName}}/krl" download="krl.bin">Download KRL</a></p>
</div>
{{end}}
{{end}}

View File

@@ -0,0 +1,60 @@
{{define "title"}} - SSH Certificate: {{.Cert.Serial}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>SSH Certificate: <code>{{.Cert.Serial}}</code></h2>
<div class="page-meta">
<a href="/sshca">&larr; SSH CA</a>
</div>
</div>
<div class="card">
<div class="card-title">Details</div>
<table class="kv-table">
<tbody>
<tr><th>Serial</th><td><code>{{.Cert.Serial}}</code></td></tr>
<tr><th>Type</th><td>{{.Cert.CertType}}</td></tr>
<tr><th>Key ID</th><td>{{.Cert.KeyID}}</td></tr>
<tr><th>Principals</th><td>{{range .Cert.Principals}}<code>{{.}}</code> {{end}}</td></tr>
<tr><th>Profile</th><td>{{if .Cert.Profile}}<a href="/sshca/profile/{{.Cert.Profile}}">{{.Cert.Profile}}</a>{{else}}&mdash;{{end}}</td></tr>
<tr><th>Issued By</th><td>{{.Cert.IssuedBy}}</td></tr>
<tr><th>Issued At</th><td>{{.Cert.IssuedAt}}</td></tr>
<tr><th>Expires At</th><td>{{.Cert.ExpiresAt}}</td></tr>
<tr>
<th>Revoked</th>
<td>
{{if .Cert.Revoked}}
<span class="badge badge-danger">Revoked</span>
{{if .Cert.RevokedAt}} at {{.Cert.RevokedAt}}{{end}}
{{if .Cert.RevokedBy}} by {{.Cert.RevokedBy}}{{end}}
{{else}}
No
{{end}}
</td>
</tr>
</tbody>
</table>
</div>
{{if .Cert.CertData}}
<div class="card">
<div class="card-title">Certificate</div>
<textarea rows="6" class="pem-input" readonly>{{.Cert.CertData}}</textarea>
</div>
{{end}}
{{if .IsAdmin}}
<div class="card">
<div class="card-title">Admin Actions</div>
{{if not .Cert.Revoked}}
<form method="post" action="/sshca/cert/{{.Cert.Serial}}/revoke" style="display:inline;">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Revoke certificate {{.Cert.Serial}}?')">Revoke</button>
</form>
{{end}}
<form method="post" action="/sshca/cert/{{.Cert.Serial}}/delete" style="display:inline; margin-left: 0.5rem;">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Permanently delete certificate {{.Cert.Serial}}?')">Delete</button>
</form>
</div>
{{end}}
{{end}}

View File

@@ -0,0 +1,104 @@
{{define "title"}} - Profile: {{.Profile.Name}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>Profile: {{.Profile.Name}}</h2>
<div class="page-meta">
<a href="/sshca">&larr; SSH CA</a>
</div>
</div>
<div class="card">
<div class="card-title">Configuration</div>
<table class="kv-table">
<tbody>
<tr><th>Name</th><td>{{.Profile.Name}}</td></tr>
<tr><th>Max TTL</th><td>{{if .Profile.MaxTTL}}{{.Profile.MaxTTL}}{{else}}&mdash;{{end}}</td></tr>
<tr>
<th>Allowed Principals</th>
<td>
{{if .Profile.AllowedPrincipals}}
{{range .Profile.AllowedPrincipals}}<code>{{.}}</code> {{end}}
{{else}}
(any)
{{end}}
</td>
</tr>
<tr>
<th>Extensions</th>
<td>
{{if .Profile.Extensions}}
{{range $k, $v := .Profile.Extensions}}<code>{{$k}}</code> {{end}}
{{else}}
(none)
{{end}}
</td>
</tr>
<tr>
<th>Critical Options</th>
<td>
{{if .Profile.CriticalOptions}}
{{range $k, $v := .Profile.CriticalOptions}}<code>{{$k}}{{if $v}}={{$v}}{{end}}</code> {{end}}
{{else}}
(none)
{{end}}
</td>
</tr>
</tbody>
</table>
</div>
{{if .IsAdmin}}
<div class="card">
<div class="card-title">Edit Profile</div>
<details>
<summary>Update configuration</summary>
<form method="post" action="/sshca/profile/{{.Profile.Name}}/update">
{{csrfField}}
<div class="form-group">
<label for="edit_max_ttl">Max TTL</label>
<input type="text" id="edit_max_ttl" name="max_ttl" value="{{.Profile.MaxTTL}}" placeholder="8760h">
</div>
<div class="form-group">
<label for="edit_principals">Allowed Principals <small style="text-transform:none;letter-spacing:0;">(one per line)</small></label>
<textarea id="edit_principals" name="allowed_principals" rows="3">{{range .Profile.AllowedPrincipals}}{{.}}
{{end}}</textarea>
</div>
<div class="form-group">
<label>Extensions</label>
<div class="checkbox-group">
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-pty"{{if index .Profile.Extensions "permit-pty"}} checked{{end}}> permit-pty</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-agent-forwarding"{{if index .Profile.Extensions "permit-agent-forwarding"}} checked{{end}}> permit-agent-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-port-forwarding"{{if index .Profile.Extensions "permit-port-forwarding"}} checked{{end}}> permit-port-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-X11-forwarding"{{if index .Profile.Extensions "permit-X11-forwarding"}} checked{{end}}> permit-X11-forwarding</label>
<label class="checkbox-label"><input type="checkbox" name="extensions" value="permit-user-rc"{{if index .Profile.Extensions "permit-user-rc"}} checked{{end}}> permit-user-rc</label>
</div>
</div>
<details>
<summary>Critical Options</summary>
<div class="form-row">
<div class="form-group">
<label for="edit_force_cmd">Force Command</label>
<input type="text" id="edit_force_cmd" name="force_command" value="{{index .Profile.CriticalOptions "force-command"}}">
</div>
<div class="form-group">
<label for="edit_source_addr">Source Address</label>
<input type="text" id="edit_source_addr" name="source_address" value="{{index .Profile.CriticalOptions "source-address"}}">
</div>
</div>
</details>
<div class="form-actions">
<button type="submit">Update Profile</button>
</div>
</form>
</details>
</div>
<div class="card">
<div class="card-title">Admin Actions</div>
<form method="post" action="/sshca/profile/{{.Profile.Name}}/delete">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Delete profile {{.Profile.Name}}?')">Delete Profile</button>
</form>
</div>
{{end}}
{{end}}

265
web/templates/transit.html Normal file
View File

@@ -0,0 +1,265 @@
{{define "title"}} - Transit: {{.MountName}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>Transit: {{.MountName}}</h2>
<div class="page-meta">
<a href="/dashboard">&larr; Dashboard</a>
</div>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<div class="card">
<div class="card-title">Named Keys</div>
{{if .Keys}}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Keys}}
<tr>
<td><a href="/transit/key/{{.Name}}">{{.Name}}</a></td>
<td><a href="/transit/key/{{.Name}}">View</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No keys configured.</p>
{{end}}
{{if .IsAdmin}}
<details style="margin-top: 1rem;">
<summary>Create Key</summary>
<form method="post" action="/transit/key/create">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="key_name">Name</label>
<input type="text" id="key_name" name="name" placeholder="payments" required>
</div>
<div class="form-group">
<label for="key_type">Type</label>
<select id="key_type" name="type">
<option value="aes256-gcm">aes256-gcm (default)</option>
<option value="chacha20-poly">chacha20-poly</option>
<option value="ed25519">ed25519</option>
<option value="ecdsa-p256">ecdsa-p256</option>
<option value="ecdsa-p384">ecdsa-p384</option>
<option value="hmac-sha256">hmac-sha256</option>
<option value="hmac-sha512">hmac-sha512</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit">Create Key</button>
</div>
</form>
</details>
{{end}}
</div>
<div class="card">
<div class="card-title">Encrypt</div>
{{if .EncryptResult}}
<div class="success">
<p>Encrypted successfully.</p>
<div class="form-group">
<label>Ciphertext</label>
<textarea rows="3" class="pem-input" readonly>{{.EncryptResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/transit/encrypt">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="enc_key">Key</label>
<select id="enc_key" name="key" required>
<option value="">&mdash; select key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="enc_ctx">Context <small style="text-transform:none;letter-spacing:0;">(optional AAD)</small></label>
<input type="text" id="enc_ctx" name="context" placeholder="base64-encoded">
</div>
</div>
<div class="form-group">
<label for="enc_plaintext">Plaintext <small style="text-transform:none;letter-spacing:0;">(base64)</small></label>
<textarea id="enc_plaintext" name="plaintext" rows="3" class="pem-input" required></textarea>
</div>
<div class="form-actions">
<button type="submit">Encrypt</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Decrypt</div>
{{if .DecryptResult}}
<div class="success">
<p>Decrypted successfully.</p>
<div class="form-group">
<label>Plaintext (base64)</label>
<textarea rows="3" class="pem-input" readonly>{{.DecryptResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/transit/decrypt">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="dec_key">Key</label>
<select id="dec_key" name="key" required>
<option value="">&mdash; select key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="dec_ctx">Context <small style="text-transform:none;letter-spacing:0;">(optional)</small></label>
<input type="text" id="dec_ctx" name="context">
</div>
</div>
<div class="form-group">
<label for="dec_ciphertext">Ciphertext</label>
<textarea id="dec_ciphertext" name="ciphertext" rows="3" class="pem-input" placeholder="metacrypt:v1:..." required></textarea>
</div>
<div class="form-actions">
<button type="submit">Decrypt</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Rewrap</div>
{{if .RewrapResult}}
<div class="success">
<p>Re-wrapped successfully.</p>
<div class="form-group">
<label>New Ciphertext</label>
<textarea rows="3" class="pem-input" readonly>{{.RewrapResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/transit/rewrap">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="rew_key">Key</label>
<select id="rew_key" name="key" required>
<option value="">&mdash; select key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="rew_ctx">Context <small style="text-transform:none;letter-spacing:0;">(optional)</small></label>
<input type="text" id="rew_ctx" name="context">
</div>
</div>
<div class="form-group">
<label for="rew_ciphertext">Ciphertext</label>
<textarea id="rew_ciphertext" name="ciphertext" rows="3" class="pem-input" placeholder="metacrypt:v1:..." required></textarea>
</div>
<div class="form-actions">
<button type="submit">Rewrap</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Sign</div>
{{if .SignResult}}
<div class="success">
<p>Signed successfully.</p>
<div class="form-group">
<label>Signature</label>
<textarea rows="3" class="pem-input" readonly>{{.SignResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/transit/sign">
{{csrfField}}
<div class="form-group">
<label for="sig_key">Key</label>
<select id="sig_key" name="key" required>
<option value="">&mdash; select signing key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="sig_input">Input <small style="text-transform:none;letter-spacing:0;">(base64)</small></label>
<textarea id="sig_input" name="input" rows="3" class="pem-input" required></textarea>
</div>
<div class="form-actions">
<button type="submit">Sign</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Verify</div>
{{if .VerifyResult}}
<div class="{{if .VerifyResult}}success{{else}}error{{end}}">
{{if .VerifyResult}}<p>Signature is valid.</p>{{else}}<p>Signature is invalid.</p>{{end}}
</div>
{{end}}
<form method="post" action="/transit/verify">
{{csrfField}}
<div class="form-group">
<label for="ver_key">Key</label>
<select id="ver_key" name="key" required>
<option value="">&mdash; select signing key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="ver_input">Input <small style="text-transform:none;letter-spacing:0;">(base64)</small></label>
<textarea id="ver_input" name="input" rows="3" class="pem-input" required></textarea>
</div>
<div class="form-group">
<label for="ver_sig">Signature</label>
<textarea id="ver_sig" name="signature" rows="3" class="pem-input" placeholder="metacrypt:v1:..." required></textarea>
</div>
<div class="form-actions">
<button type="submit">Verify</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">HMAC</div>
{{if .HMACResult}}
<div class="success">
<p>HMAC computed successfully.</p>
<div class="form-group">
<label>HMAC</label>
<textarea rows="2" class="pem-input" readonly>{{.HMACResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/transit/hmac">
{{csrfField}}
<div class="form-group">
<label for="hmac_key">Key</label>
<select id="hmac_key" name="key" required>
<option value="">&mdash; select HMAC key &mdash;</option>
{{range .Keys}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
</div>
<div class="form-group">
<label for="hmac_input">Input <small style="text-transform:none;letter-spacing:0;">(base64)</small></label>
<textarea id="hmac_input" name="input" rows="3" class="pem-input" required></textarea>
</div>
<div class="form-actions">
<button type="submit">Generate HMAC</button>
</div>
</form>
</div>
{{end}}

View File

@@ -0,0 +1,103 @@
{{define "title"}} - Key: {{.Key.Name}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>Key: {{.Key.Name}}</h2>
<div class="page-meta">
<a href="/transit">&larr; Transit</a>
</div>
</div>
<div class="card">
<div class="card-title">Key Details</div>
<table class="kv-table">
<tbody>
<tr><th>Name</th><td>{{.Key.Name}}</td></tr>
<tr><th>Type</th><td><code>{{.Key.Type}}</code></td></tr>
<tr><th>Current Version</th><td>{{.Key.CurrentVersion}}</td></tr>
<tr><th>Min Decryption Version</th><td>{{.Key.MinDecryptionVersion}}</td></tr>
<tr><th>Allow Deletion</th><td>{{if .Key.AllowDeletion}}yes{{else}}no{{end}}</td></tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">Key Versions</div>
{{if .Key.Versions}}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Version</th>
</tr>
</thead>
<tbody>
{{range .Key.Versions}}
<tr>
<td>{{.}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No version information available.</p>
{{end}}
</div>
{{if .PublicKey}}
<div class="card">
<div class="card-title">Public Key</div>
<textarea rows="4" class="pem-input" readonly>{{.PublicKey}}</textarea>
</div>
{{end}}
{{if .IsAdmin}}
<div class="card">
<div class="card-title">Admin Actions</div>
<form method="post" action="/transit/key/{{.Key.Name}}/rotate" style="display:inline;">
{{csrfField}}
<button type="submit" onclick="return confirm('Rotate key {{.Key.Name}}?')">Rotate Key</button>
</form>
<details style="margin-top: 1rem;">
<summary>Update Config</summary>
<form method="post" action="/transit/key/{{.Key.Name}}/config">
{{csrfField}}
<div class="form-row">
<div class="form-group">
<label for="cfg_min_decrypt">Min Decryption Version</label>
<input type="number" id="cfg_min_decrypt" name="min_decryption_version" value="{{.Key.MinDecryptionVersion}}" min="0">
</div>
<div class="form-group">
<label>&nbsp;</label>
<label class="checkbox-label"><input type="checkbox" name="allow_deletion"{{if .Key.AllowDeletion}} checked{{end}}> Allow Deletion</label>
</div>
</div>
<div class="form-actions">
<button type="submit">Update Config</button>
</div>
</form>
</details>
<details style="margin-top: 1rem;">
<summary>Trim Old Versions</summary>
<form method="post" action="/transit/key/{{.Key.Name}}/trim">
{{csrfField}}
<p>Trims key versions below the current min_decryption_version.</p>
<div class="form-actions">
<button type="submit" class="btn-danger" onclick="return confirm('Trim old versions of key {{.Key.Name}}? This cannot be undone.')">Trim Versions</button>
</div>
</form>
</details>
{{if .Key.AllowDeletion}}
<div style="margin-top: 1rem;">
<form method="post" action="/transit/key/{{.Key.Name}}/delete">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Permanently delete key {{.Key.Name}}? This cannot be undone.')">Delete Key</button>
</form>
</div>
{{end}}
</div>
{{end}}
{{end}}

162
web/templates/user.html Normal file
View File

@@ -0,0 +1,162 @@
{{define "title"}} - User Crypto: {{.MountName}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>User Crypto: {{.MountName}}</h2>
<div class="page-meta">
<a href="/dashboard">&larr; Dashboard</a>
</div>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<div class="card">
<div class="card-title">Your Key</div>
{{if .OwnKey}}
<table class="kv-table">
<tbody>
<tr><th>Username</th><td>{{.OwnKey.Username}}</td></tr>
<tr><th>Algorithm</th><td><code>{{.OwnKey.Algorithm}}</code></td></tr>
<tr><th>Public Key</th><td><code style="word-break:break-all;">{{.OwnKey.PublicKey}}</code></td></tr>
</tbody>
</table>
{{else}}
<p>You have no keypair registered.</p>
<form method="post" action="/user/register">
{{csrfField}}
<div class="form-actions">
<button type="submit">Register</button>
</div>
</form>
{{end}}
</div>
<div class="card">
<div class="card-title">Encrypt</div>
{{if .EncryptResult}}
<div class="success">
<p>Encrypted successfully.</p>
<div class="form-group">
<label>Envelope (JSON)</label>
<textarea rows="8" class="pem-input" readonly>{{.EncryptResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/user/encrypt">
{{csrfField}}
<div class="form-group">
<label for="enc_recipients">Recipients <small style="text-transform:none;letter-spacing:0;">(one username per line)</small></label>
<textarea id="enc_recipients" name="recipients" rows="2" required></textarea>
</div>
<div class="form-group">
<label for="enc_plaintext">Plaintext <small style="text-transform:none;letter-spacing:0;">(base64)</small></label>
<textarea id="enc_plaintext" name="plaintext" rows="3" class="pem-input" required></textarea>
</div>
<div class="form-group">
<label for="enc_metadata">Metadata <small style="text-transform:none;letter-spacing:0;">(optional, authenticated but unencrypted)</small></label>
<input type="text" id="enc_metadata" name="metadata" placeholder="">
</div>
<div class="form-actions">
<button type="submit">Encrypt</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Decrypt</div>
{{if .DecryptResult}}
<div class="success">
<p>Decrypted successfully. Sender: <code>{{.DecryptResult.Sender}}</code>{{if .DecryptResult.Metadata}} &mdash; Metadata: {{.DecryptResult.Metadata}}{{end}}</p>
<div class="form-group">
<label>Plaintext (base64)</label>
<textarea rows="3" class="pem-input" readonly>{{.DecryptResult.Plaintext}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/user/decrypt">
{{csrfField}}
<div class="form-group">
<label for="dec_envelope">Envelope (JSON)</label>
<textarea id="dec_envelope" name="envelope" rows="6" class="pem-input" required></textarea>
</div>
<div class="form-actions">
<button type="submit">Decrypt</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Re-Encrypt</div>
{{if .ReEncryptResult}}
<div class="success">
<p>Re-encrypted successfully.</p>
<div class="form-group">
<label>Updated Envelope (JSON)</label>
<textarea rows="8" class="pem-input" readonly>{{.ReEncryptResult}}</textarea>
</div>
</div>
{{end}}
<form method="post" action="/user/re-encrypt">
{{csrfField}}
<div class="form-group">
<label for="reenc_envelope">Envelope (JSON)</label>
<textarea id="reenc_envelope" name="envelope" rows="6" class="pem-input" required></textarea>
</div>
<div class="form-actions">
<button type="submit">Re-Encrypt</button>
</div>
</form>
</div>
{{if .OwnKey}}
<div class="card">
<div class="card-title">Key Rotation</div>
<p>Rotating your key generates a new keypair. Existing envelopes encrypted to your old key will become unreadable unless re-encrypted first.</p>
<form method="post" action="/user/rotate">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Rotate your key? Existing envelopes will be unreadable unless re-encrypted first.')">Rotate Key</button>
</form>
</div>
{{end}}
<div class="card">
<div class="card-title">Registered Users</div>
{{if .Users}}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Username</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Users}}
<tr>
<td><a href="/user/key/{{.}}">{{.}}</a></td>
<td><a href="/user/key/{{.}}">View Key</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No users registered.</p>
{{end}}
{{if .IsAdmin}}
<details style="margin-top: 1rem;">
<summary>Provision User</summary>
<form method="post" action="/user/provision">
{{csrfField}}
<div class="form-group">
<label for="prov_username">Username</label>
<input type="text" id="prov_username" name="username" placeholder="alice" required>
</div>
<div class="form-actions">
<button type="submit">Provision</button>
</div>
</form>
</details>
{{end}}
</div>
{{end}}

View File

@@ -0,0 +1,30 @@
{{define "title"}} - User Key: {{.KeyInfo.Username}}{{end}}
{{define "content"}}
<div class="page-header">
<h2>User Key: {{.KeyInfo.Username}}</h2>
<div class="page-meta">
<a href="/user">&larr; User Crypto</a>
</div>
</div>
<div class="card">
<div class="card-title">Public Key</div>
<table class="kv-table">
<tbody>
<tr><th>Username</th><td>{{.KeyInfo.Username}}</td></tr>
<tr><th>Algorithm</th><td><code>{{.KeyInfo.Algorithm}}</code></td></tr>
<tr><th>Public Key</th><td><code style="word-break:break-all;">{{.KeyInfo.PublicKey}}</code></td></tr>
</tbody>
</table>
</div>
{{if .IsAdmin}}
<div class="card">
<div class="card-title">Admin Actions</div>
<form method="post" action="/user/delete/{{.KeyInfo.Username}}">
{{csrfField}}
<button type="submit" class="btn-danger" onclick="return confirm('Delete user {{.KeyInfo.Username}} and their keys? This cannot be undone.')">Delete User</button>
</form>
</div>
{{end}}
{{end}}