Add options to trigger CRL generation and upload a Root CRL (#53)

This commit is contained in:
Arjan H
2023-06-11 12:09:14 +02:00
parent 9f77d1a308
commit 2b81d2d3dd
7 changed files with 336 additions and 13 deletions

View File

@@ -2,6 +2,15 @@
set -e
if [ -e data/root-ca.crl ] && [ ! -e /var/www/html/crl/root-ca.crl ]; then
cp -p data/root-ca.crl /var/www/html/crl/root-ca.crl
touch /var/www/html/crl
fi
if [ -e data/root-ca.crl ] && [ data/root-ca.crl -nt /var/www/html/crl/root-ca.crl ]; then
cp -p data/root-ca.crl /var/www/html/crl/root-ca.crl
touch /var/www/html/crl
fi
cd /var/www/html
if [ crl/ -nt certs/index.html ]; then
echo "Updating certs/index.html with latest CRL info..."

View File

@@ -239,6 +239,14 @@ case $txt in
nohup /labca/install -b $branch &>>$LOGFILE
fi
;;
"gen-issuer-crl")
cd /opt/boulder
docker compose exec -i boulder ./bin/boulder crl-updater --config labca/config/crl-updater.json -runOnce -debug-addr :18021 &>>$LOGFILE
/opt/labca/checkcrl &>>$LOGFILE
;;
"check-crl")
/opt/labca/checkcrl &>>$LOGFILE
;;
*)
echo "Unknown command '$txt'. ERROR!"
exit 1

View File

@@ -183,6 +183,9 @@ rm -f test-root.p8
if [ -e $PKI_INT_CERT_BASE.key ]; then
cp -p $PKI_INT_CERT_BASE.key test-ca.key
if [ ! -e $PKI_INT_CERT_BASE.key.der ]; then
openssl pkey -in $PKI_INT_CERT_BASE.key -out $PKI_INT_CERT_BASE.key.der -outform der
fi
cp -p $PKI_INT_CERT_BASE.key.der test-ca.key.der
cp -p $PKI_INT_CERT_BASE.pem test-ca.pem
openssl rsa -in $PKI_INT_CERT_BASE.key -pubout > test-ca.pubkey.pem 2>/dev/null || openssl ec -in $PKI_INT_CERT_BASE.key -pubout > test-ca.pubkey.pem
@@ -190,6 +193,9 @@ if [ -e $PKI_INT_CERT_BASE.key ]; then
fi
if [ -e $PKI_ROOT_CERT_BASE.key ]; then
cp -p $PKI_ROOT_CERT_BASE.key test-root.key
if [ ! -e $PKI_ROOT_CERT_BASE.key.der ]; then
openssl pkey -in $PKI_ROOT_CERT_BASE.key -out $PKI_ROOT_CERT_BASE.key.der -outform der
fi
cp -p $PKI_ROOT_CERT_BASE.key.der test-root.key.der
openssl rsa -in $PKI_ROOT_CERT_BASE.key -pubout > test-root.pubkey.pem 2>/dev/null || openssl ec -in $PKI_ROOT_CERT_BASE.key -pubout > test-root.pubkey.pem
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in test-root.key -out test-root.p8

View File

@@ -664,7 +664,6 @@ func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) err
keyFileExists = false
}
if keyFileExists {
// TODO: make option in GUI to trigger crl creation!
if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
return reportError(err)
}

View File

@@ -1039,6 +1039,95 @@ func _checkUpdatesHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(res)
}
func generateCRLHandler(w http.ResponseWriter, r *http.Request, isRoot bool) {
res := struct {
Success bool
Errors map[string]string
}{Success: true, Errors: make(map[string]string)}
if isRoot {
path := "data/"
certBase := "root-ca"
keyFileExists := true
if _, err := os.Stat(path + certBase + ".key"); os.IsNotExist(err) {
keyFileExists = false
}
if keyFileExists {
if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
res.Success = false
res.Errors["CRL"] = "Could not generate Root CRL - see logs"
}
} else {
if r.Form.Get("rootkey") == "" {
res.Success = false
res.Errors["CRL"] = "NO_ROOT_KEY"
} else {
rootci := &CertificateInfo{
IsRoot: true,
Key: r.Form.Get("rootkey"),
Passphrase: r.Form.Get("rootpassphrase"),
}
if !rootci.StoreRootKey(path) {
res.Success = false
res.Errors["CRL"] = rootci.Errors["Modal"]
} else {
// Generate CRL now that we have the key
if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
res.Success = false
res.Errors["CRL"] = "Could not generate Root CRL - see logs"
}
// Remove the Root Key if we want to keep it offline
if viper.GetBool("keep_root_offline") {
if _, err := os.Stat(path + certBase + ".key"); !os.IsNotExist(err) {
fmt.Println("Removing private Root key from the system...")
if _, err := exeCmd("rm " + path + certBase + ".key"); err != nil {
log.Printf("_certCreate: error deleting root key: %v", err)
}
}
if _, err := os.Stat(path + certBase + ".key.der"); !os.IsNotExist(err) {
if _, err := exeCmd("rm " + path + certBase + ".key.der"); err != nil {
log.Printf("_certCreate: error deleting root key (DER format): %v", err)
}
}
}
}
}
}
_hostCommand(w, r, "check-crl")
} else { // !isRoot
if !_hostCommand(w, r, "gen-issuer-crl") {
res.Success = false
res.Errors["CRL"] = "Failed to generate CRL - see logs"
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func uploadCRLHandler(w http.ResponseWriter, r *http.Request) {
res := struct {
Success bool
Errors map[string]string
}{Success: true, Errors: make(map[string]string)}
rootci := &CertificateInfo{
IsRoot: true,
CRL: r.Form.Get("crl"),
}
if !rootci.StoreCRL("data/") {
res.Success = false
res.Errors["CRL"] = rootci.Errors["Modal"]
}
_hostCommand(w, r, "check-crl")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string) bool {
if action == "backup-restore" || action == "backup-delete" || action == "backup-now" {
_backupHandler(w, r)
@@ -1080,6 +1169,21 @@ func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string)
return true
}
if action == "upload-root-crl" {
uploadCRLHandler(w, r)
return true
}
if action == "gen-root-crl" {
generateCRLHandler(w, r, true)
return true
}
if action == "gen-issuer-crl" {
generateCRLHandler(w, r, false)
return true
}
return false
}
@@ -1113,6 +1217,9 @@ func _managePost(w http.ResponseWriter, r *http.Request) {
"send-email",
"version-check",
"version-update",
"upload-root-crl",
"gen-root-crl",
"gen-issuer-crl",
} {
if a == action {
actionKnown = true
@@ -1599,7 +1706,8 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
if r.Form.Get("revertroot") != "" {
// From issuer certificate creation page it is possible to remove the root again and start over
exeCmd("rm data/root-ca.key") // Does not necessarily exist
exeCmd("rm data/root-ca.key") // Does not necessarily exist
exeCmd("rm data/root-ca.key.der") // Does not necessarily exist
if _, err := exeCmd("rm data/root-ca.pem"); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
@@ -1614,13 +1722,8 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
} else if r.Form.Get("ack-rootkey") == "yes" {
// Root Key was shown, do we need to keep it online?
if r.Form.Get("keep-root-online") == "true" {
sess, _ := sessionStore.Get(r, "labca")
sess.Values["root-online"] = true
if err := sess.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
}
viper.Set("keep_root_offline", r.Form.Get("keep-root-online") != "true")
viper.WriteConfig()
// Undo what setupHandler did when showing the public key...
_, errPem := os.Stat("data/root-ca.pem")
@@ -1792,13 +1895,18 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
log.Printf("_certCreate: could not calculate IssuerNameID: %v", err)
}
if session.Values["root-online"] != true {
if viper.GetBool("keep_root_offline") {
if _, err := os.Stat(path + "../root-ca.key"); !os.IsNotExist(err) {
fmt.Println("Removing private Root key from the system...")
if _, err := exeCmd("rm " + path + "../root-ca.key"); err != nil {
log.Printf("_certCreate: error deleting root key: %v", err)
}
}
if _, err := os.Stat(path + "../root-ca.key.der"); !os.IsNotExist(err) {
if _, err := exeCmd("rm " + path + "../root-ca.key.der"); err != nil {
log.Printf("_certCreate: error deleting root key (DER format): %v", err)
}
}
}
}

View File

@@ -170,7 +170,7 @@
step, we will delete the key from the system.<br/>
When we need the key for certain operations in the future, we will ask for it.</p>
<textarea class="form-control" rows="10" cols="80" readonly>{{ .RootKey }}</textarea>
<input type="checkbox" id="modal-keep-root-online"> Keep the root key on the LabCA server (UNSAFE)<br/>
<input type="checkbox" id="modal-keep-root-online"> Keep the root key on the LabCA server (INSECURE)<br/>
{{ with .CertificateInfo.Errors.Modal }}
<span class="error">{{ . }}</span><br/>
{{ end }}
@@ -199,7 +199,7 @@
{{ if .GetRootKey }}
<div id="modal-root-key" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-csr-content">
<div class="modal-content modal-root-key-content">
<h4>Root Key Required</h4>
<p>Please provide the Root CA key file in PEM format. As soon as we are done with it, it will be removed from the system again.</p>
<textarea class="form-control" id="modal-rootkey" rows="10" cols="80"></textarea>

View File

@@ -17,6 +17,9 @@
<li class="">
<a data-toggle="tab" href="#certs">Certificates</a>
</li>
<li class="">
<a data-toggle="tab" href="#crls">CRLs</a>
</li>
<li class="">
<a data-toggle="tab" href="#config">Config</a>
</li>
@@ -155,7 +158,12 @@
<br/>
<form role="form">
<div class="form-group">
<label for="root-cb">Export content:</label><br/>
<p><b>Download certificates:</b><br/>
<a href="/certs/root-ca.der">root-ca.der</a> | <a href="/certs/root-ca.pem">root-ca.pem</a><br/>
<a href="/certs/ca-int.der">ca-int.der</a> | <a href="/certs/ca-int.pem">ca-int.pem</a>
<hr></p>
<label for="root-cb">Export certificates + keys:</label><br/>
<input type="checkbox" class="export-cb" id="root-cb" value="root"></input>
Root Certificate (<a href="#" data-toggle="collapse" data-target="#root-details" title="View Root Certificate details"><small>Details </small><i class="fa fa-fw fa-eye" id="root-show"></i><i class="fa fa-fw fa-eye-slash hidden" id="root-hide"></i></a>)
<pre id="root-details" class="collapse">{{ .RootDetails }}</pre>
@@ -187,6 +195,33 @@
</form>
</div>
<div class="tab-pane fade" id="crls">
<br/>
<div class="row">
<div class="col-lg-3 col-md-6 col-sm-8">
<table class="table table-bordered mb15 p48">
<tbody>
<tr>
<td class="vmiddle">Root CRL</td>
<td class="vmiddle">
<button class="btn btn-outline btn-success mt5" type="button" id="gen-root-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
<button class="btn btn-outline btn-success mt5" type="button" id="upload-root-crl" title="Upload a root CRL">Upload</button>
</td>
</tr>
<tr>
<td class="vmiddle">Issuer CRL</td>
<td class="vmiddle">
<button class="btn btn-outline btn-success mt5" type="button" id="gen-issuer-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
</td>
</tr>
</tbody>
</table>
<span class="hidden" id="root-crl-result"></span>
<span class="hidden" id="issuer-crl-result"></span>
</div>
</div>
</div>
<div class="tab-pane fade" id="config">
<br/>
<form role="form">
@@ -334,6 +369,40 @@
</div>
</div>
</div>
<div id="modal-root-key" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-root-key-content">
<h4>Root Key Required</h4>
<p>Please provide the Root CA key file in PEM format. As soon as we are done with it, it will be removed from the system again.</p>
<textarea class="form-control" id="modal-rootkey" rows="10" cols="80" required></textarea>
<div class="form-group">
<label for="modal-rootpassphrase">Passphrase (optional):</label>
<input class="form-control" type="password" id="modal-rootpassphrase" value="">
</div>
{{ with .CertificateInfo.Errors.Modal }}
<span class="error">{{ . }}</span><br/>
{{ end }}
<input class="btn btn-default btn-reg" value="Upload" id="modal-root-key-upload"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-rootkey">Cancel</button>
</div>
</div>
</div>
<div id="modal-crl" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-crl-content">
<h4>CRL</h4>
<p>Please provide the CRL for the Root CA.</p>
<textarea class="form-control" id="modal-crl-val" rows="10" cols="80" required>{{ .CRL }}</textarea>
{{ with .CertificateInfo.Errors.Modal }}
<span class="error">{{ . }}</span><br/>
{{ end }}
<input class="btn btn-default btn-reg" value="OK" id="modal-crl-done"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-crl">Cancel</button>
</div>
</div>
</div>
{{end}}
{{ define "tail" }}
@@ -385,6 +454,8 @@
$(".email-error").hide();
$("#update-email-result").hide();
$("#send-email-result").hide();
$("#root-crl-result").hide();
$("#issuer-crl-result").hide();
if ( $(evt.target).attr("id") == "backup-now") {
$.ajax(window.location.href, {
@@ -778,6 +849,128 @@
$('#modal-spinner').modal('hide');
});
} else if ( $(evt.target).attr("id") == "gen-issuer-crl") {
$.ajax(window.location.href, {
method: "POST",
data: {
action: $(evt.target).attr("id"),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully generated Issuer CRL.<br/>";
$("#issuer-crl-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else {
console.log("***DEBUG*** 1 ", data);
$("#issuer-crl-result").removeClass("hidden").removeClass("success").show().text(data.Errors["CRL"]).addClass("error");
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#issuer-crl-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "gen-root-crl") {
$('#modal-root-key').modal('hide');
$.ajax(window.location.href, {
method: "POST",
data: {
action: $(evt.target).attr("id"),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully generated Root CRL.<br/>";
$("#root-crl-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else if (data.Errors["CRL"] == "NO_ROOT_KEY") {
$('#modal-root-key').modal('show');
return false;
} else {
console.log("***DEBUG*** 2 ", data);
$("#root-crl-result").removeClass("hidden").removeClass("success").show().text(data.Errors["CRL"]).addClass("error");
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#root-crl-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "modal-root-key-upload") {
$('#modal-root-key').modal('hide');
$.ajax(window.location.href, {
method: "POST",
data: {
action: 'gen-root-crl',
rootkey: $("#modal-rootkey").val(),
rootpassphrase: $("#modal-rootpassphrase").val(),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
$('.modal-backdrop').remove();
if (data.Success) {
var msg = "Successfully generated Root CRL.<br/>";
$("#root-crl-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else if (data.Errors["CRL"] == "NO_ROOT_KEY") {
$('#modal-root-key').modal('show');
return false;
} else {
console.log("***DEBUG*** 3 ", data);
$("#root-crl-result").removeClass("hidden").removeClass("success").show().text(data.Errors["CRL"]).addClass("error");
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#root-crl-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "upload-root-crl") {
$('#modal-spinner').modal('hide');
$('#modal-crl').modal('show');
return false;
} else if ( $(evt.target).attr("id") == "modal-crl-done") {
$('#modal-crl').modal('hide');
$.ajax(window.location.href, {
method: "POST",
data: {
action: 'upload-root-crl',
crl: $("#modal-crl-val").val(),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully uploaded Root CRL.<br/>";
$("#root-crl-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else {
console.log("***DEBUG*** 4 ", data);
$("#root-crl-result").removeClass("hidden").removeClass("success").show().text(data.Errors["CRL"]).addClass("error");
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#root-crl-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "cancel-rootkey" || $(evt.target).attr("id") == "cancel-crl") {
$('#modal-spinner').modal('hide');
$('#modal-crl').modal('hide');
$('#modal-root-key').modal('hide');
return false;
} else {
$.ajax(window.location.href, {
method: "POST",