Better information extraction from x509 command (#20)

- Fixes some issues with IPv6-only build on Forgejo
- Adds experimental Dockerfile for devemopment deployment
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
This commit is contained in:
Ghost 2025-04-21 10:16:26 +00:00 committed by MarkL4G
parent 0cac57dd15
commit 2b640d7578
19 changed files with 2429 additions and 939 deletions

View file

@ -0,0 +1,30 @@
name: Build
on:
pull_request:
branches:
- main
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env:
HTTP_PROXY: http://6.fsn1-1.forsaken-ashbirds.net:8888
HTTPS_PROXY: http://6.fsn1-1.forsaken-ashbirds.net:8888
GRADLE_OPTS: -Dhttp.proxyHost=6.fsn1-1.forsaken-ashbirds.net -Dhttp.proxyPort=8888 -Dhttps.proxyHost=6.fsn1-1.forsaken-ashbirds.net -Dhttps.proxyPort=8888
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- uses: https://github.com/actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- uses: https://github.com/gradle/actions/setup-gradle@v4
- name: Build gradle project
run: ./gradlew build

View file

@ -0,0 +1,33 @@
name: Check formatting
on:
pull_request:
branches:
- main
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env:
HTTP_PROXY: http://6.fsn1-1.forsaken-ashbirds.net:8888
HTTPS_PROXY: http://6.fsn1-1.forsaken-ashbirds.net:8888
GRADLE_OPTS: -Dhttp.proxyHost=6.fsn1-1.forsaken-ashbirds.net -Dhttp.proxyPort=8888 -Dhttps.proxyHost=6.fsn1-1.forsaken-ashbirds.net -Dhttps.proxyPort=8888
jobs:
check-formatting:
runs-on: ubuntu-24.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- uses: https://github.com/actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- uses: https://github.com/gradle/actions/setup-gradle@v4
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version: 20
- name: Run formatting check
run: ./gradlew spotlessCheck

View file

@ -1,37 +0,0 @@
name: Build
on:
pull_request:
branches:
- main
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- name: Update environment variables based on PR labels
id: pr-env
uses: actions/github-script@v7
with:
script: |
const labels = context.payload.pull_request.labels.map(label => label.name);
console.log('PR labels:', labels);
const springDebug = labels.includes('pr-debug');
return { SPRING_DEBUG: '' + springDebug };
result-encoding: json
- uses: gradle/actions/setup-gradle@v4
- name: Build gradle project
run: ./gradlew build
env:
ORG_GRADLE_PROJECT_SPRING_DEBUG: ${{ fromJSON(steps.pr-env.outputs.result).SPRING_DEBUG }}

View file

@ -1,28 +0,0 @@
name: Check formatting
on:
pull_request:
branches:
- main
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
check-formatting:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run formatting check
run: ./gradlew spotlessCheck

34
Dockerfile Normal file
View file

@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1.7-labs
FROM node:22 AS fe-build
RUN mkdir /mnt/workspace
WORKDIR /mnt/workspace
RUN cd /mnt/workspace
COPY ./frontend /mnt/workspace
RUN npm install --global pnpm \
&& pnpm install \
&& pnpm run build
FROM gradle AS be-build
RUN mkdir /mnt/workspace
WORKDIR /mnt/workspace
RUN cd /mnt/workspace
COPY --exclude=./frontend . /mnt/workspace
RUN ./gradlew build -x test
FROM eclipse-temurin:21-jre-alpine
ENV USE_SYSTEM_CA_CERTS=1
RUN mkdir /opt/home-cert-assistant
WORKDIR /opt/home-cert-assistant
COPY --from=fe-build /mnt/workspace/dist /opt/home-cert-assistant/frontend
COPY --from=be-build /mnt/workspace/build/home-cert-assistant-0.0.1-SNAPSHOT.jar /opt/home-cert-assistant/
CMD ["java", "-jar", "/opt/home-cert-assistant/home-cert-assistant-0.0.1-SNAPSHOT.jar"]

View file

@ -1,15 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
Home: typeof import('./src/components/Home.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View file

@ -13,30 +13,38 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"core-js": "^3.37.1", "core-js": "^3.41.0",
"roboto-fontface": "*", "roboto-fontface": "*",
"vue": "^3.4.31", "vue": "^3.5.13",
"vuetify": "^3.6.14" "vuetify": "^3.8.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.24.0",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.1",
"@types/node": "^22.9.0", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-basic-ssl": "^2.0.0",
"@vue/eslint-config-typescript": "^14.1.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.5.1", "@vue/eslint-config-typescript": "^14.5.0",
"eslint": "^9.14.0", "@vue/language-server": "^2.2.8",
"eslint-plugin-vue": "^9.30.0", "@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.1", "eslint": "^9.24.0",
"sass": "1.77.8", "eslint-plugin-vue": "^10.0.0",
"sass-embedded": "^1.77.8", "msw": "^2.7.4",
"typescript": "~5.6.3", "npm-run-all2": "^7.0.2",
"unplugin-fonts": "^1.1.1", "sass": "1.86.3",
"unplugin-vue-components": "^0.27.2", "sass-embedded": "^1.86.3",
"unplugin-vue-router": "^0.10.0", "typescript": "^5.8.3",
"vite": "^5.4.10", "unplugin-fonts": "^1.3.1",
"vite-plugin-vuetify": "^2.0.3", "unplugin-vue-components": "^28.5.0",
"vue-router": "^4.4.0", "unplugin-vue-router": "^0.12.0",
"vue-tsc": "^2.1.10" "vite": "^6.2.6",
} "vite-plugin-vuetify": "^2.1.1",
"vue-router": "^4.5.0",
"vue-tsc": "^2.2.8"
},
"engines": {
"node": "^22.14",
"pnpm": "^10.7"
},
"engineStrict": true
} }

2862
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -73,7 +73,9 @@
rounded="lg" rounded="lg"
title="Internet certificate support" title="Internet certificate support"
variant="text" variant="text"
/> >
{{ loggedInUser?.name }}
</v-card>
</v-col> </v-col>
</v-row> </v-row>
</v-responsive> </v-responsive>
@ -81,5 +83,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, ref} from "vue";
const loggedInUser = ref<{name: string} | null>(null);
onMounted(async () => {
loggedInUser.value = await (await fetch('http://localhost:8080/api/v1/users/self')).json();
})
// //
</script> </script>

View file

@ -1,9 +1,14 @@
import { registerPlugins } from '@/plugins' import { registerPlugins } from '@/plugins'
import { setupBackendMocking } from '@/plugins/mock-service-worker';
import App from './App.vue' import App from './App.vue'
import { createApp } from 'vue' import { createApp } from 'vue'
if (import.meta.env.DEV) {
setupBackendMocking().then(worker => worker.start());
}
const app = createApp(App) const app = createApp(App)
registerPlugins(app) registerPlugins(app)

View file

@ -0,0 +1,19 @@
import {http, HttpResponse} from "msw";
import {setupWorker} from "msw/browser";
export const MOCK_BASEURL = "http://localhost:8080";
const setupHandlers = async () => {
return [
http.get(`${MOCK_BASEURL}/api/v1/users/self`, () => HttpResponse.json({
id: window.crypto.randomUUID(),
name: 'Max Mustermann',
mail: 'testmail@example.com',
})),
]
}
export async function setupBackendMocking() {
const handlers = await setupHandlers();
return setupWorker(...handlers);
}

View file

@ -1,6 +1,5 @@
{ {
"files": [], "files": [],
"include": ["./typed-router.d.ts"],
"references": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"

View file

@ -2,8 +2,8 @@ import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue' import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import ViteFonts from 'unplugin-fonts/vite' import ViteFonts from 'unplugin-fonts/vite'
import VueRouter from 'unplugin-vue-router/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

View file

@ -3,16 +3,18 @@ package de.mlessmann.certassist.models;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.AccessLevel; import java.util.Objects;
import lombok.Data; import lombok.*;
import lombok.RequiredArgsConstructor; import org.hibernate.proxy.HibernateProxy;
import lombok.Setter;
@Entity @Entity
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) }) @Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) })
@Data @Getter
@Setter
@ToString
@RequiredArgsConstructor @RequiredArgsConstructor
public class Certificate { public class Certificate {
@ -27,11 +29,18 @@ public class Certificate {
private String trustingAuthority; private String trustingAuthority;
@Min(1) /**
* <ul>
* <li>-1 = no requested key length is known (might happen with imported certificates)</li>
* <li>0 = no key is available for this certificate (might happen with trusted third party certificates)</li>
* <li>> 1 = The key length in bits used for the private key of this certificate</li>
* </ul>
*/
@Min(-1)
private int requestedKeyLength; private int requestedKeyLength;
@Min(1) private OffsetDateTime notBefore;
private int requestedValidityDays; private OffsetDateTime notAfter;
@NotNull @NotNull
private String subjectCommonName; private String subjectCommonName;
@ -44,6 +53,7 @@ public class Certificate {
private String subjectLocality; private String subjectLocality;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@ToString.Exclude
private List<CertificateExtension> certificateExtension = new ArrayList<>(); private List<CertificateExtension> certificateExtension = new ArrayList<>();
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135 //@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
@ -60,4 +70,26 @@ public class Certificate {
@Column(nullable = false) @Column(nullable = false)
private String fingerprint; private String fingerprint;
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy
? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass()
: o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy
? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass()
: this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
Certificate that = (Certificate) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy
? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode()
: getClass().hashCode();
}
} }

View file

@ -22,6 +22,10 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -65,6 +69,21 @@ public class OpenSSLService {
private static final Pattern FINGERPRINT_EXTRACTOR = Pattern.compile( private static final Pattern FINGERPRINT_EXTRACTOR = Pattern.compile(
"^(?<algo>[0-9a-zA-Z]+) (?i)Fingerprint(?-i)=(?<finger>[a-z:A-Z0-9]+)" "^(?<algo>[0-9a-zA-Z]+) (?i)Fingerprint(?-i)=(?<finger>[a-z:A-Z0-9]+)"
); );
private final DateTimeFormatter OSSL_DATE_TIME = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendValue(ChronoField.YEAR, 4)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.appendLiteral(' ')
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.appendOffset("+HH:MM:ss", "Z")
.toFormatter();
private static final String OSSL_ENV_KEY_PW = "KEY_PASS"; private static final String OSSL_ENV_KEY_PW = "KEY_PASS";
private static final String OSSL_ARG_KEY_PW = "env:" + OSSL_ENV_KEY_PW; private static final String OSSL_ARG_KEY_PW = "env:" + OSSL_ENV_KEY_PW;
private final AtomicBoolean versionLogged = new AtomicBoolean(false); private final AtomicBoolean versionLogged = new AtomicBoolean(false);
@ -560,7 +579,7 @@ public class OpenSSLService {
@NonNull @NonNull
@SneakyThrows @SneakyThrows
public CertificateInfo getCertificateInfo(String pemContent) { public X509CertificateInfo getCertificateInfo(String pemContent) {
StartedProcess infoProc = null; StartedProcess infoProc = null;
try (var input = new ByteArrayInputStream(pemContent.getBytes())) { try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start(); infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start();
@ -581,7 +600,7 @@ public class OpenSSLService {
} }
@NonNull @NonNull
public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException { public X509CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
requireNonNull(path, "Certificate file must be provided to read the info."); requireNonNull(path, "Certificate file must be provided to read the info.");
try { try {
return getCertificateInfo(Files.readString(path)); return getCertificateInfo(Files.readString(path));
@ -620,9 +639,12 @@ public class OpenSSLService {
} }
} }
private CertificateInfo getCertificateInfo(String[] lines) { /**
var builder = CertificateInfo.builder(); * Reads the OpenSSL x509 command output to provide the requested certificate information
boolean hasIssuer = false; */
private X509CertificateInfo getCertificateInfo(String[] lines) {
var builder = X509CertificateInfo.builder();
List<CertificateInfoExtension> extensions = new ArrayList<>();
for (int i = 0; i < lines.length; i++) { for (int i = 0; i < lines.length; i++) {
String line = lines[i]; String line = lines[i];
@ -634,9 +656,8 @@ public class OpenSSLService {
subjectBuilder = readSubjectInfo(line, subjectBuilder); subjectBuilder = readSubjectInfo(line, subjectBuilder);
line = lines[++i]; line = lines[++i];
} }
builder = builder.subject(subjectBuilder); builder = builder.subject(subjectBuilder.build());
} else if (line.startsWith("issuer=")) { } else if (line.startsWith("issuer=")) {
hasIssuer = true;
var issuerBuilder = CertificateInfoSubject.builder(); var issuerBuilder = CertificateInfoSubject.builder();
line = lines[++i]; line = lines[++i];
@ -645,14 +666,24 @@ public class OpenSSLService {
line = lines[++i]; line = lines[++i];
} }
builder = builder.issuer(issuerBuilder); builder = builder.issuer(issuerBuilder.build());
} else if (line.startsWith("X509v3 Subject Alternative Name")) { } else if (line.startsWith("X509v3 Subject Alternative Name")) {
String[] altNames = lines[++i].split(","); String[] altNames = lines[++i].split(",");
builder = builder.extension(CertificateInfoExtension.builder().alternativeDnsNames(altNames)); extensions.add(CertificateInfoExtension.builder().alternativeDnsNames(altNames).build());
} else if (line.startsWith("serial=")) {
builder = builder.serial(line.substring("serial=".length()));
} else if (line.startsWith("notBefore=")) {
String notBeforeStr = line.substring("notBefore=".length());
var notBefore = OffsetDateTime.parse(notBeforeStr, OSSL_DATE_TIME);
builder = builder.notBefore(notBefore);
} else if (line.startsWith("notAfter=")) {
String notAfterStr = line.substring("notAfter=".length());
var notAfter = OffsetDateTime.parse(notAfterStr, OSSL_DATE_TIME);
builder = builder.notAfter(notAfter);
} }
} }
builder = builder.type(hasIssuer ? RequestType.NORMAL_CERTIFICATE : RequestType.STANDALONE_CERTIFICATE); builder = builder.extensions(extensions);
return builder.build(); return builder.build();
} }
@ -730,7 +761,8 @@ public class OpenSSLService {
"-nameopt", "-nameopt",
"sep_multiline", "sep_multiline",
"-nameopt", "-nameopt",
"lname" "lname",
"-modulus"
) )
); );
command.addAll(Arrays.asList(additArgs)); command.addAll(Arrays.asList(additArgs));

View file

@ -0,0 +1,39 @@
package de.mlessmann.certassist.openssl;
import de.mlessmann.certassist.models.CertificateInfoExtension;
import de.mlessmann.certassist.models.CertificateInfoSubject;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
import org.springframework.lang.Nullable;
@Builder
public record X509CertificateInfo(
CertificateInfoSubject subject,
@Nullable CertificateInfoSubject issuer,
String serial,
OffsetDateTime notBefore,
OffsetDateTime notAfter,
List<CertificateInfoExtension> extensions
) {
public X509CertificateInfo {
Objects.requireNonNull(subject);
Objects.requireNonNull(serial);
Objects.requireNonNull(notBefore);
Objects.requireNonNull(notAfter);
}
@Override
public List<CertificateInfoExtension> extensions() {
if (extensions == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(extensions);
}
public boolean hasExtensions() {
return extensions != null && !extensions.isEmpty();
}
}

View file

@ -7,6 +7,7 @@ import de.mlessmann.certassist.repositories.CertificateRepository;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,7 +56,9 @@ public class CertificateCreationService {
certificate.setSubjectCommonName(certificateInfo.getSubject().getCommonName()); certificate.setSubjectCommonName(certificateInfo.getSubject().getCommonName());
certificate.setTrustingAuthority(certificateInfo.getTrustingAuthority()); certificate.setTrustingAuthority(certificateInfo.getTrustingAuthority());
certificate.setRequestedKeyLength(certificateInfo.getRequestedKeyLength()); certificate.setRequestedKeyLength(certificateInfo.getRequestedKeyLength());
certificate.setRequestedValidityDays(certificateInfo.getRequestedValidityDays()); certificate.setNotBefore(OffsetDateTime.now());
certificate.setNotAfter(OffsetDateTime.now().plusDays(certificateInfo.getRequestedValidityDays()));
final CertificateInfoSubject subjectInfo = certificateInfo.getSubject(); final CertificateInfoSubject subjectInfo = certificateInfo.getSubject();
certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress()); certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress());
certificate.setSubjectOrganization(subjectInfo.getOrganization()); certificate.setSubjectOrganization(subjectInfo.getOrganization());
@ -74,6 +77,43 @@ public class CertificateCreationService {
return certificate; return certificate;
} }
private Certificate createEntityFromInfo(X509CertificateInfo info) {
final Certificate certificate = new Certificate();
certificate.setType(
mapCertificateRequestType(
info.issuer() != null
? CertificateInfo.RequestType.NORMAL_CERTIFICATE
: CertificateInfo.RequestType.STANDALONE_CERTIFICATE
)
);
certificate.setSubjectCommonName(info.subject().getCommonName());
if (info.issuer() != null) {
certificate.setTrustingAuthority(info.issuer().getCommonName());
}
certificate.setRequestedKeyLength(-1);
certificate.setNotBefore(info.notBefore());
certificate.setNotAfter(info.notAfter());
final CertificateInfoSubject subjectInfo = info.subject();
certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress());
certificate.setSubjectOrganization(subjectInfo.getOrganization());
certificate.setSubjectOrganizationalUnit(subjectInfo.getOrganizationalUnit());
certificate.setSubjectCountry(subjectInfo.getCountry());
certificate.setSubjectState(subjectInfo.getState());
certificate.setSubjectLocality(subjectInfo.getLocality());
if (info.hasExtensions()) {
final CertificateInfoExtension extension = info.extensions().getFirst();
if (extension != null) {
final CertificateExtension certificateExtension = new CertificateExtension();
certificateExtension.setIdentifier("alternativeNames");
certificateExtension.setValue(String.join(",", extension.getAlternativeDnsNames()));
certificate.setCertificateExtension(List.of(certificateExtension));
}
}
return certificate;
}
@NonNull @NonNull
public Certificate importCertificate( public Certificate importCertificate(
@NonNull Path certificate, @NonNull Path certificate,
@ -82,8 +122,8 @@ public class CertificateCreationService {
) { ) {
try { try {
String fingerprint = openSSLService.getCertificateFingerprint(certificate); String fingerprint = openSSLService.getCertificateFingerprint(certificate);
var generatedRequest = openSSLService.getCertificateInfo(certificate); Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(certificate));
Certificate entity = createEntityFromRequest(generatedRequest); entity.setRequestedKeyLength(-1);
entity.setFingerprint(fingerprint); entity.setFingerprint(fingerprint);
entity.setCert(Files.readAllBytes(certificate)); entity.setCert(Files.readAllBytes(certificate));
if (keyFile != null) { if (keyFile != null) {
@ -113,8 +153,7 @@ public class CertificateCreationService {
int endIdx = endMatcher.end(); int endIdx = endMatcher.end();
String singleCert = pemContent.substring(startIdx, endIdx); String singleCert = pemContent.substring(startIdx, endIdx);
String fingerprint = openSSLService.getCertificateFingerprint(singleCert); String fingerprint = openSSLService.getCertificateFingerprint(singleCert);
var generatedRequest = openSSLService.getCertificateInfo(singleCert); Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(singleCert));
Certificate entity = createEntityFromRequest(generatedRequest);
entity.setFingerprint(fingerprint); entity.setFingerprint(fingerprint);
entity.setCert(singleCert.getBytes()); entity.setCert(singleCert.getBytes());
certsInBundle.put(fingerprint, entity); certsInBundle.put(fingerprint, entity);

View file

@ -1,5 +1,6 @@
package de.mlessmann.certassist; package de.mlessmann.certassist;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -7,10 +8,11 @@ import de.mlessmann.certassist.models.CertificateInfo;
import de.mlessmann.certassist.models.CertificateInfo.RequestType; import de.mlessmann.certassist.models.CertificateInfo.RequestType;
import de.mlessmann.certassist.models.CertificateInfoExtension; import de.mlessmann.certassist.models.CertificateInfoExtension;
import de.mlessmann.certassist.models.CertificateInfoSubject; import de.mlessmann.certassist.models.CertificateInfoSubject;
import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
import de.mlessmann.certassist.openssl.CertificateProvider;
import de.mlessmann.certassist.openssl.OpenSSLService;
import de.mlessmann.certassist.service.ExecutableResolver; import de.mlessmann.certassist.service.ExecutableResolver;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@ -60,7 +62,7 @@ class TestOpenSSLService {
assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), cert.certificatePath())) assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), cert.certificatePath()))
.withFailMessage(ERR_VERIFY_FAILED) .withFailMessage(ERR_VERIFY_FAILED)
.isTrue(); .isTrue();
assertThat(certificateCreator.isKeyEncrypted(cert.certificateKeyPath())) assertThat(certificateCreator.isKeyEncrypted(requireNonNull(cert.certificateKeyPath())))
.withFailMessage(ERR_NOT_ENCRYPTED) .withFailMessage(ERR_NOT_ENCRYPTED)
.isTrue(); .isTrue();
@ -85,12 +87,10 @@ class TestOpenSSLService {
when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert); when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert);
try (var childCert = certificateCreator.createCertificate(childRequest)) { try (var childCert = certificateCreator.createCertificate(childRequest)) {
Path fullchain = childCert.fullchainPath(); Path fullchain = childCert.fullchainPath();
assertThat( assertThat(certificateCreator.verifyCertificate(requireNonNull(fullchain), cert.certificatePath()))
certificateCreator.verifyCertificate(Objects.requireNonNull(fullchain), cert.certificatePath())
)
.withFailMessage(ERR_VERIFY_FAILED) .withFailMessage(ERR_VERIFY_FAILED)
.isTrue(); .isTrue();
assertThat(certificateCreator.isKeyEncrypted(childCert.certificateKeyPath())) assertThat(certificateCreator.isKeyEncrypted(requireNonNull(childCert.certificateKeyPath())))
.withFailMessage(ERR_NOT_ENCRYPTED) .withFailMessage(ERR_NOT_ENCRYPTED)
.isTrue(); .isTrue();
} }
@ -105,20 +105,23 @@ class TestOpenSSLService {
var request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImportCA.pem")); var request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImportCA.pem"));
assertThat(request).isNotNull(); assertThat(request).isNotNull();
assertThat(request.getSubject().getCommonName()).isEqualTo("test.home"); assertThat(request.subject().getCommonName()).isEqualTo("test.home");
assertThat(request.getSubject().getCountry()).isEqualTo("DE"); assertThat(request.subject().getCountry()).isEqualTo("DE");
assertThat(request.getSubject().getState()).isEqualTo("SH"); assertThat(request.subject().getState()).isEqualTo("SH");
assertThat(request.getSubject().getLocality()).isEqualTo("HH"); assertThat(request.subject().getLocality()).isEqualTo("HH");
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats"); assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
assertThat(request.getExtension()).isNull(); assertThat(request.notBefore()).isEqualTo("2024-11-22T18:57:40Z");
assertThat(request.notAfter()).isEqualTo("2025-11-22T18:57:40Z");
assertThat(request.extensions()).isEmpty();
request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImport.pem")); request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImport.pem"));
assertThat(request).isNotNull(); assertThat(request).isNotNull();
assertThat(request.getSubject().getCommonName()).isEqualTo("test.local"); assertThat(request.subject().getCommonName()).isEqualTo("test.local");
assertThat(request.getSubject().getCountry()).isEqualTo("DE"); assertThat(request.subject().getCountry()).isEqualTo("DE");
assertThat(request.getSubject().getState()).isEqualTo("SH"); assertThat(request.subject().getState()).isEqualTo("SH");
assertThat(request.getSubject().getLocality()).isEqualTo("HH"); assertThat(request.subject().getLocality()).isEqualTo("HH");
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats"); assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
assertThat(request.getExtension().getAlternativeDnsNames()).containsExactly("test2.local", "test3.local"); assertThat(request.extensions().getFirst().getAlternativeDnsNames())
.containsExactly("test2.local", "test3.local");
} }
} }

View file

@ -6,6 +6,7 @@ import de.mlessmann.certassist.models.Certificate;
import de.mlessmann.certassist.models.CertificateExtension; import de.mlessmann.certassist.models.CertificateExtension;
import de.mlessmann.certassist.models.CertificateType; import de.mlessmann.certassist.models.CertificateType;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -33,7 +34,8 @@ class CertificateRepositoryTest {
certificate.setSubjectCommonName("test-cn"); certificate.setSubjectCommonName("test-cn");
certificate.setType(CertificateType.SIGNED_CERT); certificate.setType(CertificateType.SIGNED_CERT);
certificate.setRequestedKeyLength(1); certificate.setRequestedKeyLength(1);
certificate.setRequestedValidityDays(1); certificate.setNotBefore(OffsetDateTime.now());
certificate.setNotAfter(OffsetDateTime.now().plusDays(1));
return certificate; return certificate;
} }