Skip to content
Snippets Groups Projects
Verified Commit ac2cdff5 authored by Peter Oettig's avatar Peter Oettig
Browse files

Implement CSR and private key generation with almost everything hardcoded

parent 47208b4d
No related branches found
No related tags found
No related merge requests found
Pipeline #416725 passed
......@@ -16,6 +16,7 @@
"@mdi/font": "^7.4.47",
"core-js": "^3.41.0",
"jwt-decode": "^4.0.0",
"pkijs": "^3.2.4",
"roboto-fontface": "^0.10.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.2",
......
......@@ -19,91 +19,88 @@
<v-container v-else-if="subPage === RequestCertificatePage.Request">
<v-row>
<v-col>
<v-stepper
v-model="step"
:items="[$t(`request.common.steps.emailAddresses.title`), $t(`request.common.steps.checkRequest.title`)]"
>
<template #[`item.1`]>
<v-card>
<v-card-title>{{ $t("request.common.steps.emailAddresses.title") }}</v-card-title>
<v-card-text>
<v-row>
<v-col>
<hyphenated-justified-text>
{{ $t(`request.${certificateType}.steps.emailAddresses.description`) }}
</hyphenated-justified-text>
</v-col>
</v-row>
<v-row>
<v-col>
<v-form ref="emailAddressesForm" validate-on="blur">
<slot />
</v-form>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<template #[`item.2`]>
<v-card>
<v-card-title>{{ $t(`request.common.steps.checkRequest.title`) }}</v-card-title>
<v-card-text>
<v-row>
<v-col>
<hyphenated-justified-text>
{{ $t(`request.${certificateType}.steps.checkRequest.description`) }}
</hyphenated-justified-text>
</v-col>
</v-row>
<v-stepper v-model="step" :mobile="$vuetify.display.smAndDown">
<v-stepper-header>
<template v-for="(stepName, idx) in stepNames" :key="idx">
<v-stepper-item
:title="step !== idx + 1 ? $t(`request.common.steps.${stepName}.title`) : ''"
:value="idx + 1"
:complete="step > idx + 1"
/>
<v-divider v-if="idx !== stepNames.length - 1"></v-divider>
</template>
</v-stepper-header>
<v-stepper-window>
<v-stepper-window-item v-for="(stepName, idx) in stepNames" :key="idx" :value="idx + 1">
<v-card>
<v-card-title>{{ $t(`request.common.steps.${stepName}.title`) }}</v-card-title>
<v-card-text>
<v-row>
<v-col>
<hyphenated-justified-text>
{{ $t(`request.${certificateType}.steps.${stepName}.description`) }}
</hyphenated-justified-text>
</v-col>
</v-row>
<v-row>
<v-col>
<v-form v-if="stepName === 'emailAddresses'" ref="emailAddressesForm" validate-on="blur">
<slot name="emailAddressesFormContent" />
</v-form>
<v-row>
<v-col>
<v-list>
<v-list-item v-if="request.identification" prepend-icon="fas fa-signature">
<!-- Currently, the name from the IDM is used for the certificate -->
<v-list-item-title>{{ store.user?.firstname }} {{ store.user?.lastname }}</v-list-item-title>
<v-list-item-subtitle>
{{ $t(`request.${certificateType}.steps.checkRequest.requestInfo.commonName`) }}
</v-list-item-subtitle>
</v-list-item>
<v-list-item
v-for="(emailAddress, idx) in request.emailAddresses"
:key="idx"
density="compact"
:prepend-icon="`${isPrimaryAddress(emailAddress, idx) ? 'fas' : 'far'} fa-envelope`"
>
<v-list-item-title>
{{ emailAddress }}
</v-list-item-title>
<v-list-item-subtitle>
{{
isPrimaryAddress(emailAddress, idx)
? $t(`request.common.steps.checkRequest.requestInfo.primaryEmailAddress`)
: $t(`request.common.steps.checkRequest.requestInfo.emailAddress`)
}}
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<template #next>
<v-btn v-if="step === 1" :disabled="!request.formValid" @click="step++">
{{ $t("$vuetify.stepper.next") }}
</v-btn>
<v-btn
v-if="step === 2"
variant="elevated"
color="primary"
:loading="submitting"
:disabled="false"
@click="requestCertificate"
>
{{ $t("$vuetify.stepper.submit") }}
</v-btn>
</template>
<v-list v-if="stepName === 'checkRequest'">
<v-list-item v-if="request.identification" prepend-icon="fas fa-signature">
<!-- Currently, the name from the IDM is used for the certificate -->
<v-list-item-title>
{{ store.user?.firstname }} {{ store.user?.lastname }}
</v-list-item-title>
<v-list-item-subtitle>
{{ $t(`request.${certificateType}.steps.checkRequest.requestInfo.commonName`) }}
</v-list-item-subtitle>
</v-list-item>
<v-list-item
v-for="(emailAddress, emailIdx) in request.emailAddresses"
:key="emailIdx"
density="compact"
:prepend-icon="`${isPrimaryAddress(emailAddress, emailIdx) ? 'fas' : 'far'} fa-envelope`"
>
<v-list-item-title>
{{ emailAddress }}
</v-list-item-title>
<v-list-item-subtitle>
{{
isPrimaryAddress(emailAddress, emailIdx)
? $t(`request.common.steps.checkRequest.requestInfo.primaryEmailAddress`)
: $t(`request.common.steps.checkRequest.requestInfo.emailAddress`)
}}
</v-list-item-subtitle>
</v-list-item>
</v-list>
<slot v-else :name="stepName"></slot>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-stepper-window-item>
</v-stepper-window>
<v-stepper-actions @click:prev="step--">
<template #next>
<v-btn
v-if="step === stepNames.length"
variant="elevated"
color="primary"
:loading="submitting"
:disabled="false"
@click="requestCertificate"
>
{{ $t("$vuetify.stepper.submit") }}
</v-btn>
<v-btn v-else :disabled="!request.formValid" @click="step++">
{{ $t("$vuetify.stepper.next") }}
</v-btn>
</template>
</v-stepper-actions>
</v-stepper>
</v-col>
</v-row>
......@@ -210,6 +207,7 @@ export enum RequestCertificateSubPage {
export type RequestCertificatePageData = {
subPage: RequestCertificateSubPage;
step: number;
stepNames: string[];
request: {
identification: Identification | null;
primaryEmailAddress: string | null;
......@@ -238,6 +236,14 @@ export default defineComponent({
type: Function,
required: true,
},
additionalStepNames: {
type: Array<string>,
default: [],
},
skipDefaultSteps: {
type: Boolean,
default: false,
},
},
setup() {
return {
......@@ -250,6 +256,9 @@ export default defineComponent({
return {
subPage: RequestCertificateSubPage.Loading as RequestCertificateSubPage,
step: 1,
stepNames: (this.$props.skipDefaultSteps ? [] : ["emailAddresses", "checkRequest"]).concat(
this.$props.additionalStepNames
),
request: {
identification: null as Identification | null,
primaryEmailAddress: null as string | null,
......@@ -299,15 +308,3 @@ export default defineComponent({
},
});
</script>
<style>
@media (max-width: 599px) {
.v-stepper-window {
margin: 0.5rem !important;
}
.v-stepper-item__content {
display: none;
}
}
</style>
......@@ -165,7 +165,7 @@ export const de: I18nType = {
title: "E-Mail-Adressen",
},
checkRequest: {
title: "Zertifikatsantrag prüfen & Absenden",
title: "Zertifikatsantrag prüfen",
requestInfo: {
primaryEmailAddress: "Primäre (sichtbare) E-Mail-Adresse des Zertifikats",
emailAddress: "Zusätzlich im Zertifikat aufgenommene E-Mail-Adresse",
......
......@@ -164,7 +164,7 @@ export const en: I18nType = {
title: "Email Addresses",
},
checkRequest: {
title: "Check & Submit Certificate Request",
title: "Check Certificate Request",
requestInfo: {
primaryEmailAddress: "Primary (visible) email address of the certificate",
emailAddress: "Additional email address included in the certificate",
......
import { MaintenanceMode, ResponseWrapper, StoreType, TokenPayload, User } from "@/ts/types";
import { MaintenanceMode, PEMBoundaryType, ResponseWrapper, StoreType, TokenPayload, User } from "@/ts/types";
import { APIError } from "@/ts/errorTypes";
import { reactive } from "vue";
import router from "@/router";
import { jwtDecode } from "jwt-decode";
import * as pkijs from "pkijs";
import * as asn1js from "asn1js";
const API_PATH = "/api/v1";
const MODULES = import.meta.glob("/src/assets/*", { eager: true });
......@@ -176,3 +178,55 @@ export async function requestBackendWithResponseHeaders<ResponseType>(
throw apiError;
});
}
export function base64StringToPEM(base64String: string, boundaryType: PEMBoundaryType) {
const privateKeyPEMArray = [`-----BEGIN ${boundaryType}-----`];
for (let i = 0; i < base64String.length; i += 64) {
privateKeyPEMArray.push(base64String.slice(i, i + 64));
}
privateKeyPEMArray.push(`-----END ${boundaryType}-----`);
return privateKeyPEMArray.join("\n");
}
export async function generateKeyAndCSR() {
// Generate key material
const crypto = pkijs.getCrypto();
if (crypto == null) {
return null;
}
const params = {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
publicExponent: Uint8Array.from([0x1, 0x0, 0x1]), // 65537 == 2^16+1
hash: "SHA-256",
};
const keys = await crypto.generateKey(params, true, ["sign"]);
const privateKey = keys.privateKey;
const publicKey = keys.publicKey;
// Generate CSR
const csr = new pkijs.CertificationRequest();
csr.subject.typesAndValues.push(
new pkijs.AttributeTypeAndValue({
type: "2.5.4.3", // CommonName
value: new asn1js.Utf8String({ value: "ThisCNWillPreventHARICAToBreakBecauseOfAnEmptySubject" }),
})
);
await csr.subjectPublicKeyInfo.importKey(publicKey);
await csr.sign(privateKey);
// Export key as PEM
const privateKeyPKCS8 = await crypto.exportKey("pkcs8", privateKey);
const privateKeyPKCS8Base64 = window.btoa(
Array.from(new Uint8Array(privateKeyPKCS8))
.map((b) => String.fromCharCode(b))
.join("")
);
console.log(base64StringToPEM(privateKeyPKCS8Base64, PEMBoundaryType.PRIVATE_KEY));
// Return CSR as PEM
return base64StringToPEM(csr.toString("base64"), PEMBoundaryType.CERTIFICATE_REQUEST);
}
......@@ -277,3 +277,10 @@ export class ResponseWrapper<ResponseType> {
this.headers = headers;
}
}
export enum PEMBoundaryType {
CERTIFICATE = "CERTIFICATE",
CERTIFICATE_REQUEST = "CERTIFICATE REQUEST",
PRIVATE_KEY = "PRIVATE KEY",
ENCRYPTED_PRIVATE_KEY = "ENCRYPTED PRIVATE KEY",
}
......@@ -134,7 +134,7 @@ export default defineComponent({
id: "function",
icon: "people-group",
target: "/request-functional",
enabled: false,
enabled: true,
},
{
type: FunctionCardTypes.RequestCertificates,
......
......@@ -4,49 +4,56 @@
certificate-type="functional"
:certificate-request-body-getter="getCertificateRequestBody"
:start-over-extra-steps="reset"
:additional-step-names="['generateCSR']"
>
<v-row v-for="idx in mailAddressesCount" :key="idx" dense>
<v-col>
<v-text-field
v-model="requestCertificatePage.request.emailAddresses[idx - 1]"
prepend-icon="fas fa-envelope"
:label="$t('request.functional.steps.emailAddresses.emailAddressTextFieldLabel')"
:rules="[validateEmailAddress]"
:onblur="validateForm"
>
<template #append>
<v-btn
v-if="mailAddressesCount > 1"
icon="fas fa-minus"
color="red"
size="x-small"
:tabindex="idx"
@click="
mailAddressesCount--;
requestCertificatePage.request.emailAddresses.splice(idx - 1, 1);
validateForm();
"
></v-btn>
</template>
</v-text-field>
</v-col>
</v-row>
<v-row dense>
<v-col class="text-center">
<v-btn color="secondary" @click="mailAddressesCount++">
<div class="d-block d-sm-none">
<icon-with-text icon="fas fa-plus">
{{ $t("request.functional.steps.emailAddresses.addAnotherButtonShort") }}
</icon-with-text>
</div>
<div class="d-none d-sm-block">
<icon-with-text icon="fas fa-plus">
{{ $t("request.functional.steps.emailAddresses.addAnotherButton") }}
</icon-with-text>
</div>
</v-btn>
</v-col>
</v-row>
<template #emailAddressesFormContent>
<v-row v-for="idx in mailAddressesCount" :key="idx" dense>
<v-col>
<v-text-field
v-model="requestCertificatePage.request.emailAddresses[idx - 1]"
prepend-icon="fas fa-envelope"
:label="$t('request.functional.steps.emailAddresses.emailAddressTextFieldLabel')"
:rules="[validateEmailAddress]"
:onblur="validateForm"
>
<template #append>
<v-btn
v-if="mailAddressesCount > 1"
icon="fas fa-minus"
color="red"
size="x-small"
:tabindex="idx"
@click="
mailAddressesCount--;
requestCertificatePage.request.emailAddresses.splice(idx - 1, 1);
validateForm();
"
></v-btn>
</template>
</v-text-field>
</v-col>
</v-row>
<v-row dense>
<v-col class="text-center">
<v-btn color="secondary" @click="mailAddressesCount++">
<div class="d-block d-sm-none">
<icon-with-text icon="fas fa-plus">
{{ $t("request.functional.steps.emailAddresses.addAnotherButtonShort") }}
</icon-with-text>
</div>
<div class="d-none d-sm-block">
<icon-with-text icon="fas fa-plus">
{{ $t("request.functional.steps.emailAddresses.addAnotherButton") }}
</icon-with-text>
</div>
</v-btn>
</v-col>
</v-row>
</template>
<template #generateCSR>
<pre>{{ csr }}</pre>
</template>
</RequestCertificatePage>
</template>
......@@ -56,7 +63,7 @@ import RequestCertificatePage, {
RequestCertificatePageData,
RequestCertificateSubPage,
} from "@/components/RequestCertificatePage.vue";
import { store } from "@/ts/common";
import { store, generateKeyAndCSR } from "@/ts/common";
import IconWithText from "@/components/IconWithText.vue";
export default defineComponent({
......@@ -70,10 +77,19 @@ export default defineComponent({
data() {
return {
mailAddressesCount: 1,
csr: "Generating CSR...",
};
},
mounted() {
this.reset();
generateKeyAndCSR().then((csr) => {
if (csr == null) {
this.csr = "Failed generating CSR :(";
return;
}
this.csr = csr;
});
},
methods: {
reset: function () {
......
......@@ -5,21 +5,23 @@
:certificate-request-body-getter="getCertificateRequestBody"
:start-over-extra-steps="startOverExtraSteps"
>
<v-checkbox
v-model="requestCertificatePage.request.emailAddresses"
:value="store.user?.primaryEmail"
:label="store.user?.primaryEmail"
:disabled="true"
hide-details
></v-checkbox>
<v-checkbox
v-for="(email, idx) in store.user?.auxiliaryEmails"
:key="idx"
v-model="requestCertificatePage.request.emailAddresses"
:value="email"
:label="email"
hide-details
></v-checkbox>
<template #emailAddressesFormContent>
<v-checkbox
v-model="requestCertificatePage.request.emailAddresses"
:value="store.user?.primaryEmail"
:label="store.user?.primaryEmail"
:disabled="true"
hide-details
></v-checkbox>
<v-checkbox
v-for="(email, idx) in store.user?.auxiliaryEmails"
:key="idx"
v-model="requestCertificatePage.request.emailAddresses"
:value="email"
:label="email"
hide-details
></v-checkbox>
</template>
</RequestCertificatePage>
</template>
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment