Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 72 additions & 7 deletions web-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2",
"asn1js": "^3.0.10",
"axios": "^1.15.0",
"dayjs": "^1.11.18",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"jsrsasign": "^11.1.1",
"pkijs": "^3.4.0",
"pvutils": "^1.1.5",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.12.0"
Expand Down
33 changes: 33 additions & 0 deletions web-ui/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,36 @@ A React-based certificate management dashboard built with Vite, Material-UI, and
---

*This specification document provides a comprehensive overview of the Settings Dashboard project, covering architecture, features, implementation details, and development guidelines.*

---

## 2026-05-12 - Replace jsrsasign with pkijs

**Decision:** Replaced the now-EOL `jsrsasign` library with `pkijs` + `asn1js` + the browser's Web Crypto API (`crypto.subtle`).

**Why:**
- `jsrsasign` reached end-of-support on 2026-Apr-10 with unfixed open vulnerabilities; staying on it is no longer viable.
- `pkijs` (by PeculiarVentures) covers everything we use today (X.509 parsing, chain verification, private-key matching) AND the upcoming **OCSP / CRL** feature work, so we avoid bringing in a second PKI dependency later.
- Web Crypto handles all hashing and signing/verification natively, with no JS-implemented crypto primitives.

**Affected files:**
- New: `web-ui/src/utils/pkiHelpers.js` - PEM<->DER, OID-to-name maps, KeyUsage bit decoding, SAN flattening, RSA modulus bit-length, EC curve detection, private key import for PKCS#8 / PKCS#1 / SEC1.
- Rewritten: `web-ui/src/utils/certificateUtils.js`, `web-ui/src/utils/verificationUtils.js`.
- Patched: `web-ui/src/components/CertificateDetailsDialog.jsx`, `web-ui/src/components/ImportCertificateChainDialogContent.jsx`, `web-ui/src/hooks/useCertificateImport.js` - now `await` the previously-sync `verifyCertificate`, `parseCertificate`, `parseCertificateChainFromPem`.
- `web-ui/package.json` - removed `jsrsasign`, added `pkijs`, `asn1js`, `pvutils`.

**Behavioral changes:**
- `parseCertificate`, `parseCertificateChainFromPem`, `verifyCertificate`, `validateCertificateChain`, `validatePrivateKey`, `getFingerprint` are now `async`. `isValidPemCertificate` and `isValidPemPrivateKey` remain sync (structural validation only).
- **DSA private keys are no longer supported.** Web Crypto cannot import or sign with DSA. The PEM header is rejected by `isValidPemPrivateKey`, and any path that reaches `importPrivateKeyFromPem` throws a clear `"DSA private keys are not supported in this environment"` error. DSA was never a product requirement (no fixtures, tests, or product flows used it), only a side-effect of `jsrsasign`'s capabilities.
- Supported private key formats: PKCS#8 (`-----BEGIN PRIVATE KEY-----`), PKCS#1 RSA (`-----BEGIN RSA PRIVATE KEY-----`), SEC1 EC (`-----BEGIN EC PRIVATE KEY-----`, with or without a preceding `EC PARAMETERS` block as emitted by OpenSSL).
- Supported public key algorithms in certificates: RSA (`RSASSA-PKCS1-v1_5` with SHA-256) and ECDSA on P-256 / P-384 / P-521.

**Future work covered by the same dependency:**
- OCSP responder check via `pkijs.OCSPRequest` / `OCSPResponse`, AIA URL discovery from extension OID `1.3.6.1.5.5.7.1.1`.
- CRL distribution-point check via `pkijs.CertificateRevocationList`, URLs from extension OID `2.5.29.31`.
- Both will live in a new `web-ui/src/utils/revocationUtils.js` and reuse the `pkijs.Certificate` instances already produced by `parseCertificateChain`.

**Cryptography (replaces the original entry on line 15):**
- `pkijs` + `asn1js` for X.509 / PKI structure handling
- `crypto.subtle` (Web Crypto) for all hashing, signing, verification, and key import
- Supports RSA and ECDSA. DSA and Ed25519 are not supported in this environment.
2 changes: 1 addition & 1 deletion web-ui/src/components/CertificateDetailsDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default function CertificateDetailsDialog({ open, onClose, certificate })
? base64ToPrivateKeyPem(certificate.rawPrivateKey)
: null

const result = verifyCertificate(pemCertificate, privateKeyPem)
const result = await verifyCertificate(pemCertificate, privateKeyPem)
setVerificationResult(result)
} catch (error) {
setVerificationResult({
Expand Down
41 changes: 26 additions & 15 deletions web-ui/src/components/ImportCertificateChainDialogContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,33 @@ export default function ImportCertificateChainDialogContent({

// Parse certificate chain when PEM text changes
useEffect(() => {
let cancelled = false
if (pemText.trim()) {
const parsed = parseCertificateChainFromPem(pemText)
if (parsed.length === 0) {
setParseError('No valid certificates found in the provided text')
setCertificates([])
setSelectedCertificateIndex(null)
setSelectedCertificatePem(null)
} else {
setParseError(null)
setCertificates(parsed)
// Auto-select first certificate if available
if (parsed.length > 0) {
setSelectedCertificateIndex(0)
setSelectedCertificatePem(parsed[0].certificate)
}
}
parseCertificateChainFromPem(pemText)
.then(parsed => {
if (cancelled) return
if (parsed.length === 0) {
setParseError('No valid certificates found in the provided text')
setCertificates([])
setSelectedCertificateIndex(null)
setSelectedCertificatePem(null)
} else {
setParseError(null)
setCertificates(parsed)
if (parsed.length > 0) {
setSelectedCertificateIndex(0)
setSelectedCertificatePem(parsed[0].certificate)
}
}
})
.catch(err => {
if (cancelled) return
setParseError(`Failed to parse certificates: ${err.message}`)
setCertificates([])
setSelectedCertificateIndex(null)
setSelectedCertificatePem(null)
})
return () => { cancelled = true }
} else {
setParseError(null)
setCertificates([])
Expand Down
2 changes: 1 addition & 1 deletion web-ui/src/hooks/useCertificateImport.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const useCertificateImport = (targetStore, currentCertificates = null) =>
}

try {
const details = parseCertificate(pemText)
const details = await parseCertificate(pemText)
setCertificateDetails(details)

// Clear any previous errors
Expand Down
Loading
Loading