Skip to content

Commit

Permalink
Merge branch '43-regenerate-crl' into 'master'
Browse files Browse the repository at this point in the history
Resolve "Regenerate CRL"

Closes #43

See merge request bounca/bounca!33
  • Loading branch information
bjarnoldus committed Jan 31, 2024
2 parents 931e60f + 8dacc64 commit 8594d72
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 23 deletions.
23 changes: 23 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,29 @@ def validate_passphrase_out_confirmation(self, passphrase_out_confirmation):
return None


class CrlRenewSerializer(serializers.ModelSerializer):
passphrase_in = serializers.CharField(max_length=200, required=True)

class Meta:
fields = ("passphrase_in",)
model = Certificate
extra_kwargs = {
"passphrase_in": {"write_only": True},
}

def validate_passphrase_in(self, passphrase_in):
if passphrase_in:
try:
if not self.instance.is_passphrase_valid(passphrase_in):
raise serializers.ValidationError(
"Passphrase incorrect. Not allowed to update Crl list of your certificate"
)
except KeyStore.DoesNotExist:
raise serializers.ValidationError("Certificate has no cert, something went wrong during generation")
return passphrase_in
return None


class CertificateCRLSerializer(serializers.ModelSerializer):
passphrase_issuer = serializers.CharField(max_length=200, required=True)

Expand Down
46 changes: 36 additions & 10 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@

from api.authentication import AppTokenAuthentication
from api.mixins import TrapDjangoValidationErrorCreateMixin
from api.serializers import CertificateRenewSerializer, CertificateRevokeSerializer, CertificateSerializer
from api.serializers import (
CertificateRenewSerializer,
CertificateRevokeSerializer,
CertificateSerializer,
CrlRenewSerializer,
)
from x509_pki.models import Certificate, CertificateTypes, KeyStore

if settings.IS_GENERATE_FRONTEND:
Expand Down Expand Up @@ -300,16 +305,18 @@ def get(self, request, pk, *args, **kwargs):
return self._make_file_response(content, filename)


class CertificateCRLFilesView(FileView):
class CertificateCRLFilesView(FileView, UpdateAPIView):
authentication_classes = [AppTokenAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
serializer_class = CrlRenewSerializer

def get(self, request, pk, *args, **kwargs):
try:
user = self.request.user
cert = Certificate.objects.get(pk=pk, owner=user)
except Certificate.DoesNotExist:
raise Http404("File not found")
queryset = Certificate.objects.all()
model = Certificate

def get(self, request, pk, *args, **kwargs):
cert = Certificate.objects.get(pk=pk)
user = self.request.user
if cert.owner.id is not user.id:
raise Http404("Certificate not found")
if cert.type in [CertificateTypes.ROOT, CertificateTypes.INTERMEDIATE]:
if not cert.crl_distribution_url:
raise Http404("CRL Distribution is not enabled, " "no crl distribution url")
Expand All @@ -319,18 +326,37 @@ def get(self, request, pk, *args, **kwargs):
except KeyStore.DoesNotExist:
raise Http404("Certificate has no keystore, " "generation of certificate object went wrong")

matches = re.findall(r"[^\/]+\.crl(.pem)?$", cert.crl_distribution_url)
matches = re.findall(r"([^\/]+\.crl(.pem)?)$", cert.crl_distribution_url)
if not matches:
raise RuntimeError(
f"Unexpected wrong format crl distribution url: "
f"{cert.crl_distribution_url} should end with "
f"<filename>.crl"
)
filename = matches[0]
filename = matches[0][0]
response = HttpResponse(cert_crlstore.crl, content_type="application/octet-stream")
response["Content-Disposition"] = f"attachment; filename={filename}"
response["Last-Modified"] = http_date(cert_crlstore.last_update.timestamp())
response["Access-Control-Expose-Headers"] = "Content-Disposition"
return response
else:
raise ValidationError("CRL can only be generated for Root or Intermediate certificates")

def update(self, request, *args, **kwargs):
user = self.request.user
instance = self.get_object()
if instance.owner.id is not user.id:
raise Http404("Certificate not found")
partial = kwargs.pop("partial", False)

serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
instance.passphrase_in = serializer.validated_data["passphrase_in"]
self.perform_update(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

def perform_update(self, instance):
try:
return instance.renew_revocation_list()
except InternalValidationError as e:
raise ValidationError(e.message)
4 changes: 4 additions & 0 deletions bounca/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def get_services_config(path, filename="services.yaml"):
DEBUG = SERVICES["django"]["debug"]

KEY_ALGORITHM = SERVICES["certificate-engine"]["key_algorithm"].lower()

# Nr of days in the future the CRL list will expire
CRL_UPDATE_DAYS_FUTURE = 365

if KEY_ALGORITHM not in ["ed25519", "rsa"]:
raise ValueError(f"Key algorithm {KEY_ALGORITHM} not supported")

Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
The BounCA change history

## [unreleased]
* Generate CRL file updates via button, and set expire day 365 days in the future
* Added legacy pkcs12 support for macOS
* Mail template fix
* Fixed ALL option in table views
Expand Down
5 changes: 5 additions & 0 deletions front/src/api/certificates.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export default {
const headers = { Authorization: `Token ${store.getters['auth/accessToken']}` };
return axios.delete(url, { data: data, headers: headers }).then((response) => response.data);
},
renewCrl(id, data) {
const url = `${process.env.VUE_APP_ROOT_API}/api/v1/certificates/${id}/crl`;
const headers = { Authorization: `Token ${store.getters['auth/accessToken']}` };
return axios.patch(url, data, { headers: headers }).then((response) => response.data);
},
renew(id, data) {
const url = `${process.env.VUE_APP_ROOT_API}/api/v1/certificates/${id}/renew`;
const headers = { Authorization: `Token ${store.getters['auth/accessToken']}` };
Expand Down
90 changes: 88 additions & 2 deletions front/src/components/dashboard/Intermediate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,20 @@
<v-btn class=""
text
:disabled="!item.crl_distribution_url"
target="_blank"
:href="api_root + '/api/v1/certificates/' + item.id + '/crl'">
@click="downloadCRL(item.id)">
CRL
<v-icon class="" color="grey darken-2">
mdi-download
</v-icon>
</v-btn>
<v-btn class=""
text
:disabled="!item.crl_distribution_url"
@click="renewCRL(item.id)">
CRL
<v-icon class="mr-2" color="green darken-2">
mdi-sync
</v-icon>
</v-btn>
</span>
</template>
Expand Down Expand Up @@ -149,6 +160,50 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogRenewCrl" max-width="565px">
<v-card>
<v-card-title class="text-h5">Enter passphrase to renew CRL list certificate.</v-card-title>
<v-card-text>
<ValidationObserver ref="form" v-slot="{ errors }">
<ValidationProvider name="non_field_errors" vid="non_field_errors">
<v-alert
text
dense
type="error"
border="left"
v-if="errors.non_field_errors && errors.non_field_errors.length"
>
<div v-for="error in errors.non_field_errors" :key="error">{{error}}</div>
</v-alert>
</ValidationProvider>
<v-form>
<ValidationProvider name="passphrase_in" vid="passphrase_in" rules="required"
v-slot="{ errors }">
<v-text-field
prepend-icon="lock"
name="passphrase"
label="Passphrase"
id="passphrase"
v-model="renewcrl.passphrase_in"
:error-messages="errors"
:append-icon="revoke_passphrase_visible ? 'visibility' : 'visibility_off'"
@click:append="() => (revoke_passphrase_visible = !revoke_passphrase_visible)"
:type="revoke_passphrase_visible ? 'text' : 'password' "
required
></v-text-field>
</ValidationProvider>
</v-form>
</ValidationObserver>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeRenewCrl">Cancel</v-btn>
<v-btn color="blue darken-1" text
@click="renewCRLConfirm">OK</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDownloading" max-width="565px">
<v-card >
<v-container style="height: 200px;">
Expand Down Expand Up @@ -240,6 +295,9 @@ export default {
revoke: {
passphrase_issuer: null,
},
renewcrl: {
passphrase_in: null,
},
revoke_passphrase_visible: false,
dialog: false,
dialogDownloading: false,
Expand All @@ -249,6 +307,9 @@ export default {
dialogInfoText: '',
dialogInfoLoading: true,
renewCrlItem: null,
dialogRenewCrl: false,
dialogError: false,
dialogErrorText: '',
Expand Down Expand Up @@ -454,6 +515,31 @@ export default {
this.deleteItem = null;
},
renewCRL(item) {
this.renewCrlItem = item;
this.dialogRenewCrl = true;
},
renewCRLConfirm() {
if (this.renewCrlItem !== null) {
certificates.renewCrl(this.renewCrlItem, this.renewcrl)
.then(() => {
this.updateDashboard();
this.closeRenewCrl();
}).catch((r) => {
const errors = r.response.data;
this.$refs.form.setErrors(errors);
});
}
},
closeRenewCrl() {
this.renewcrl.passphrase_in = '';
this.$refs.form.reset();
this.dialogRenewCrl = false;
this.renewClrItem = null;
},
closeInfo() {
this.dialogInfo = false;
},
Expand Down
92 changes: 90 additions & 2 deletions front/src/components/dashboard/Root.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,22 @@
<v-btn class=""
text
:disabled="!item.crl_distribution_url"
target="_blank"
:href="api_root + '/api/v1/certificates/' + item.id + '/crl'">
@click="downloadCRL(item.id)">
CRL
<v-icon class="" color="grey darken-2">
mdi-download
</v-icon>
</v-btn>
<v-btn class=""
text
:disabled="!item.crl_distribution_url"
@click="renewCRL(item.id)">
CRL
<v-icon class="mr-2" color="green darken-2">
mdi-sync
</v-icon>
</v-btn>

</span>
</template>
</v-data-table>
Expand Down Expand Up @@ -134,6 +146,51 @@
</v-card-actions>
</v-card>
</v-dialog>

<v-dialog v-model="dialogRenewCrl" max-width="565px">
<v-card>
<v-card-title class="text-h5">Enter passphrase to renew CRL list certificate.</v-card-title>
<v-card-text>
<ValidationObserver ref="form" v-slot="{ errors }">
<ValidationProvider name="non_field_errors" vid="non_field_errors">
<v-alert
text
dense
type="error"
border="left"
v-if="errors.non_field_errors && errors.non_field_errors.length"
>
<div v-for="error in errors.non_field_errors" :key="error">{{error}}</div>
</v-alert>
</ValidationProvider>
<v-form>
<ValidationProvider name="passphrase_in" vid="passphrase_in" rules="required"
v-slot="{ errors }">
<v-text-field
prepend-icon="lock"
name="passphrase"
label="Passphrase"
id="passphrase"
v-model="renewcrl.passphrase_in"
:error-messages="errors"
:append-icon="revoke_passphrase_visible ? 'visibility' : 'visibility_off'"
@click:append="() => (revoke_passphrase_visible = !revoke_passphrase_visible)"
:type="revoke_passphrase_visible ? 'text' : 'password' "
required
></v-text-field>
</ValidationProvider>
</v-form>
</ValidationObserver>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeRenewCrl">Cancel</v-btn>
<v-btn color="blue darken-1" text
@click="renewCRLConfirm">OK</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDownloading" max-width="565px">
<v-card >
<v-container style="height: 200px;">
Expand Down Expand Up @@ -219,6 +276,9 @@ export default {
revoke: {
passphrase_issuer: null,
},
renewcrl: {
passphrase_in: null,
},
api_root: process.env.VUE_APP_ROOT_API,
revoke_passphrase_visible: false,
dialog: false,
Expand All @@ -229,6 +289,9 @@ export default {
dialogInfoText: '',
dialogInfoLoading: true,
renewCrlItem: null,
dialogRenewCrl: false,
dialogError: false,
dialogErrorText: '',
Expand Down Expand Up @@ -425,6 +488,31 @@ export default {
this.deleteItem = null;
},
renewCRL(item) {
this.renewCrlItem = item;
this.dialogRenewCrl = true;
},
renewCRLConfirm() {
if (this.renewCrlItem !== null) {
certificates.renewCrl(this.renewCrlItem, this.renewcrl)
.then(() => {
this.updateDashboard();
this.closeRenewCrl();
}).catch((r) => {
const errors = r.response.data;
this.$refs.form.setErrors(errors);
});
}
},
closeRenewCrl() {
this.renewcrl.passphrase_in = '';
this.$refs.form.reset();
this.dialogRenewCrl = false;
this.renewClrItem = null;
},
closeInfo() {
this.dialogInfo = false;
},
Expand Down
Empty file added x509_pki/management/__init__.py
Empty file.
Empty file.

0 comments on commit 8594d72

Please sign in to comment.