From c664e864cea87a9dbd3bc0ba04f2018f4f729bcc Mon Sep 17 00:00:00 2001 From: Will Rouesnel Date: Thu, 25 May 2023 00:21:32 +1000 Subject: [PATCH] Add storing for Client CA certificates in the database Add initial support for managing Client Certificate Authority public certificates as certificate objects in the database. The new provider type 'clientca' is defined to implement this. --- backend/internal/certificate.js | 21 ++++++++++------- backend/schema/definitions.json | 2 +- frontend/js/app/nginx/certificates/form.ejs | 18 ++++++++++++++- frontend/js/app/nginx/certificates/form.js | 25 +++++++++++++++++---- frontend/js/app/nginx/certificates/main.ejs | 1 + frontend/js/i18n/messages.json | 7 ++++-- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 88f267d6..723f94b2 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -552,13 +552,18 @@ const internalCertificate = { }) .then(() => { return new Promise((resolve, reject) => { - fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); + if (certificate.provider === 'clientca') { + // Client CAs have no private key associated, so just succeed. + resolve(); + } else { + fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } }); }); }, @@ -639,7 +644,7 @@ const internalCertificate = { upload: (access, data) => { return internalCertificate.get(access, {id: data.id}) .then((row) => { - if (row.provider !== 'other') { + if (row.provider !== 'other' && row.provider !== 'clientca') { throw new error.ValidationError('Cannot upload certificates for this type of provider'); } diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json index 4b4f3405..136b0235 100644 --- a/backend/schema/definitions.json +++ b/backend/schema/definitions.json @@ -219,7 +219,7 @@ }, "ssl_provider": { "type": "string", - "pattern": "^(letsencrypt|other)$" + "pattern": "^(letsencrypt|other|clientca)$" }, "http2_support": { "description": "HTTP2 Protocol Support", diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs index 7fc12785..6b87261d 100644 --- a/frontend/js/app/nginx/certificates/form.ejs +++ b/frontend/js/app/nginx/certificates/form.ejs @@ -173,7 +173,23 @@ - + <% } else if (provider === 'clientca') { %> + +
+
+ + +
+
+
+
+
<%- i18n('certificates', 'clientca-certificate') %>*
+
+ + +
+
+
<% } %> diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js index a56c3f8e..eb6fb708 100644 --- a/frontend/js/app/nginx/certificates/form.js +++ b/frontend/js/app/nginx/certificates/form.js @@ -45,7 +45,9 @@ module.exports = Mn.View.extend({ propagation_seconds: 'input[name="meta[propagation_seconds]"]', other_certificate_key_label: '#other_certificate_key_label', other_intermediate_certificate: '#other_intermediate_certificate', - other_intermediate_certificate_label: '#other_intermediate_certificate_label' + other_intermediate_certificate_label: '#other_intermediate_certificate_label', + clientca_certificate: '#clientca_certificate', + clientca_certificate_label: '#clientca_certificate_label' }, events: { @@ -156,6 +158,18 @@ module.exports = Mn.View.extend({ } ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]}); } + } else if (data.provider === 'clientca' && !this.model.hasSslFiles()) { + // check files are attached + if (!this.ui.clientca_certificate[0].files.length || !this.ui.clientca_certificate[0].files[0].size) { + alert('Certificate file is not attached'); + return; + } else { + if (this.ui.clientca_certificate[0].files[0].size > this.max_file_size) { + alert('Certificate file is too large (> 100kb)'); + return; + } + ssl_files.push({name: 'certificate', file: this.ui.clientca_certificate[0].files[0]}); + } } this.ui.loader_content.show(); @@ -163,14 +177,14 @@ module.exports = Mn.View.extend({ // compile file data let form_data = new FormData(); - if (data.provider === 'other' && ssl_files.length) { + if ((data.provider === 'other' || data.provider === 'clientca') && ssl_files.length) { ssl_files.map(function (file) { form_data.append(file.name, file.file); }); } new Promise(resolve => { - if (data.provider === 'other') { + if (data.provider === 'other' || data.provider === 'clientca') { resolve(App.Api.Nginx.Certificates.validate(form_data)); } else { resolve(); @@ -183,7 +197,7 @@ module.exports = Mn.View.extend({ this.model.set(result); // Now upload the certs if we need to - if (data.provider === 'other') { + if (data.provider === 'other' || data.provider === 'clientca') { return App.Api.Nginx.Certificates.upload(this.model.get('id'), form_data) .then(result => { this.model.set('meta', _.assign({}, this.model.get('meta'), result)); @@ -234,6 +248,9 @@ module.exports = Mn.View.extend({ }, 'change @ui.other_intermediate_certificate': function(e){ this.setFileName("other_intermediate_certificate_label", e) + }, + 'change @ui.clientca_certificate': function(e){ + this.setFileName("clientca_certificate_label", e) } }, setFileName(ui, e){ diff --git a/frontend/js/app/nginx/certificates/main.ejs b/frontend/js/app/nginx/certificates/main.ejs index dbd6fa85..5d49c47a 100644 --- a/frontend/js/app/nginx/certificates/main.ejs +++ b/frontend/js/app/nginx/certificates/main.ejs @@ -20,6 +20,7 @@ <% } %> diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 4f12f72b..148d3261 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -99,6 +99,7 @@ "ssl": { "letsencrypt": "Let's Encrypt", "other": "Custom", + "clientca": "Client Certificate Authority", "none": "HTTP only", "letsencrypt-email": "Email Address for Let's Encrypt", "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", @@ -185,7 +186,7 @@ "title": "SSL Certificates", "empty": "There are no SSL Certificates", "add": "Add SSL Certificate", - "form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate", + "form-title": "Add {provider, select, letsencrypt{Let's Encrypt Certificate} other{Custom Certificate} clientca{Client Certificate Authority}}", "delete": "Delete SSL Certificate", "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.", "help-title": "SSL Certificates", @@ -193,6 +194,7 @@ "other-certificate": "Certificate", "other-certificate-key": "Certificate Key", "other-intermediate-certificate": "Intermediate Certificate", + "clientca-certificate": "Certificate", "force-renew": "Renew Now", "test-reachability": "Test Server Reachability", "reachability-title": "Test Server Reachability", @@ -231,7 +233,8 @@ "pass-auth": "Pass Auth to Host", "access-add": "Add", "auth-add": "Add", - "search": "Search Access…" + "search": "Search Access…", + "client-certificates": "Client Certificates" }, "users": { "title": "Users",