Better information extraction from x509 command #20
19 changed files with 2429 additions and 939 deletions
30
.forgejo/workflows/build.yml
Normal file
30
.forgejo/workflows/build.yml
Normal 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
|
33
.forgejo/workflows/formatting.yml
Normal file
33
.forgejo/workflows/formatting.yml
Normal 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
|
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
|
@ -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 }}
|
28
.github/workflows/formatting.yml
vendored
28
.github/workflows/formatting.yml
vendored
|
@ -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
34
Dockerfile
Normal 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"]
|
15
frontend/components.d.ts
vendored
15
frontend/components.d.ts
vendored
|
@ -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']
|
||||
}
|
||||
}
|
|
@ -13,30 +13,38 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"core-js": "^3.37.1",
|
||||
"core-js": "^3.41.0",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.4.31",
|
||||
"vuetify": "^3.6.14"
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.9.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"npm-run-all2": "^7.0.1",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.77.8",
|
||||
"typescript": "~5.6.3",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"unplugin-vue-router": "^0.10.0",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/language-server": "^2.2.8",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"msw": "^2.7.4",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"sass": "1.86.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"unplugin-vue-router": "^0.12.0",
|
||||
"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
2862
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -73,7 +73,9 @@
|
|||
rounded="lg"
|
||||
title="Internet certificate support"
|
||||
variant="text"
|
||||
/>
|
||||
>
|
||||
{{ loggedInUser?.name }}
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
|
@ -81,5 +83,12 @@
|
|||
</template>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
import { setupBackendMocking } from '@/plugins/mock-service-worker';
|
||||
import App from './App.vue'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
setupBackendMocking().then(worker => worker.start());
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
|
19
frontend/src/plugins/mock-service-worker.ts
Normal file
19
frontend/src/plugins/mock-service-worker.ts
Normal 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);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"files": [],
|
||||
"include": ["./typed-router.d.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
|
|
|
@ -2,8 +2,8 @@ import Components from 'unplugin-vue-components/vite'
|
|||
import Vue from '@vitejs/plugin-vue'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
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 { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ package de.mlessmann.certassist.models;
|
|||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import java.util.Objects;
|
||||
import lombok.*;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
|
||||
@Entity
|
||||
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) })
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Certificate {
|
||||
|
||||
|
@ -27,11 +29,18 @@ public class Certificate {
|
|||
|
||||
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;
|
||||
|
||||
@Min(1)
|
||||
private int requestedValidityDays;
|
||||
private OffsetDateTime notBefore;
|
||||
private OffsetDateTime notAfter;
|
||||
|
||||
@NotNull
|
||||
private String subjectCommonName;
|
||||
|
@ -44,6 +53,7 @@ public class Certificate {
|
|||
private String subjectLocality;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@ToString.Exclude
|
||||
private List<CertificateExtension> certificateExtension = new ArrayList<>();
|
||||
|
||||
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
|
||||
|
@ -60,4 +70,26 @@ public class Certificate {
|
|||
|
||||
@Column(nullable = false)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
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.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -65,6 +69,21 @@ public class OpenSSLService {
|
|||
private static final Pattern FINGERPRINT_EXTRACTOR = Pattern.compile(
|
||||
"^(?<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_ARG_KEY_PW = "env:" + OSSL_ENV_KEY_PW;
|
||||
private final AtomicBoolean versionLogged = new AtomicBoolean(false);
|
||||
|
@ -560,7 +579,7 @@ public class OpenSSLService {
|
|||
|
||||
@NonNull
|
||||
@SneakyThrows
|
||||
public CertificateInfo getCertificateInfo(String pemContent) {
|
||||
public X509CertificateInfo getCertificateInfo(String pemContent) {
|
||||
StartedProcess infoProc = null;
|
||||
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||
infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start();
|
||||
|
@ -581,7 +600,7 @@ public class OpenSSLService {
|
|||
}
|
||||
|
||||
@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.");
|
||||
try {
|
||||
return getCertificateInfo(Files.readString(path));
|
||||
|
@ -620,9 +639,12 @@ public class OpenSSLService {
|
|||
}
|
||||
}
|
||||
|
||||
private CertificateInfo getCertificateInfo(String[] lines) {
|
||||
var builder = CertificateInfo.builder();
|
||||
boolean hasIssuer = false;
|
||||
/**
|
||||
* Reads the OpenSSL x509 command output to provide the requested certificate information
|
||||
*/
|
||||
private X509CertificateInfo getCertificateInfo(String[] lines) {
|
||||
var builder = X509CertificateInfo.builder();
|
||||
List<CertificateInfoExtension> extensions = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
|
@ -634,9 +656,8 @@ public class OpenSSLService {
|
|||
subjectBuilder = readSubjectInfo(line, subjectBuilder);
|
||||
line = lines[++i];
|
||||
}
|
||||
builder = builder.subject(subjectBuilder);
|
||||
builder = builder.subject(subjectBuilder.build());
|
||||
} else if (line.startsWith("issuer=")) {
|
||||
hasIssuer = true;
|
||||
var issuerBuilder = CertificateInfoSubject.builder();
|
||||
|
||||
line = lines[++i];
|
||||
|
@ -645,14 +666,24 @@ public class OpenSSLService {
|
|||
line = lines[++i];
|
||||
}
|
||||
|
||||
builder = builder.issuer(issuerBuilder);
|
||||
builder = builder.issuer(issuerBuilder.build());
|
||||
} else if (line.startsWith("X509v3 Subject Alternative Name")) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -730,7 +761,8 @@ public class OpenSSLService {
|
|||
"-nameopt",
|
||||
"sep_multiline",
|
||||
"-nameopt",
|
||||
"lname"
|
||||
"lname",
|
||||
"-modulus"
|
||||
)
|
||||
);
|
||||
command.addAll(Arrays.asList(additArgs));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import de.mlessmann.certassist.repositories.CertificateRepository;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -55,7 +56,9 @@ public class CertificateCreationService {
|
|||
certificate.setSubjectCommonName(certificateInfo.getSubject().getCommonName());
|
||||
certificate.setTrustingAuthority(certificateInfo.getTrustingAuthority());
|
||||
certificate.setRequestedKeyLength(certificateInfo.getRequestedKeyLength());
|
||||
certificate.setRequestedValidityDays(certificateInfo.getRequestedValidityDays());
|
||||
certificate.setNotBefore(OffsetDateTime.now());
|
||||
certificate.setNotAfter(OffsetDateTime.now().plusDays(certificateInfo.getRequestedValidityDays()));
|
||||
|
||||
final CertificateInfoSubject subjectInfo = certificateInfo.getSubject();
|
||||
certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress());
|
||||
certificate.setSubjectOrganization(subjectInfo.getOrganization());
|
||||
|
@ -74,6 +77,43 @@ public class CertificateCreationService {
|
|||
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
|
||||
public Certificate importCertificate(
|
||||
@NonNull Path certificate,
|
||||
|
@ -82,8 +122,8 @@ public class CertificateCreationService {
|
|||
) {
|
||||
try {
|
||||
String fingerprint = openSSLService.getCertificateFingerprint(certificate);
|
||||
var generatedRequest = openSSLService.getCertificateInfo(certificate);
|
||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||
Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(certificate));
|
||||
entity.setRequestedKeyLength(-1);
|
||||
entity.setFingerprint(fingerprint);
|
||||
entity.setCert(Files.readAllBytes(certificate));
|
||||
if (keyFile != null) {
|
||||
|
@ -113,8 +153,7 @@ public class CertificateCreationService {
|
|||
int endIdx = endMatcher.end();
|
||||
String singleCert = pemContent.substring(startIdx, endIdx);
|
||||
String fingerprint = openSSLService.getCertificateFingerprint(singleCert);
|
||||
var generatedRequest = openSSLService.getCertificateInfo(singleCert);
|
||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||
Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(singleCert));
|
||||
entity.setFingerprint(fingerprint);
|
||||
entity.setCert(singleCert.getBytes());
|
||||
certsInBundle.put(fingerprint, entity);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package de.mlessmann.certassist;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.CertificateInfoExtension;
|
||||
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 java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
@ -60,7 +62,7 @@ class TestOpenSSLService {
|
|||
assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), cert.certificatePath()))
|
||||
.withFailMessage(ERR_VERIFY_FAILED)
|
||||
.isTrue();
|
||||
assertThat(certificateCreator.isKeyEncrypted(cert.certificateKeyPath()))
|
||||
assertThat(certificateCreator.isKeyEncrypted(requireNonNull(cert.certificateKeyPath())))
|
||||
.withFailMessage(ERR_NOT_ENCRYPTED)
|
||||
.isTrue();
|
||||
|
||||
|
@ -85,12 +87,10 @@ class TestOpenSSLService {
|
|||
when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert);
|
||||
try (var childCert = certificateCreator.createCertificate(childRequest)) {
|
||||
Path fullchain = childCert.fullchainPath();
|
||||
assertThat(
|
||||
certificateCreator.verifyCertificate(Objects.requireNonNull(fullchain), cert.certificatePath())
|
||||
)
|
||||
assertThat(certificateCreator.verifyCertificate(requireNonNull(fullchain), cert.certificatePath()))
|
||||
.withFailMessage(ERR_VERIFY_FAILED)
|
||||
.isTrue();
|
||||
assertThat(certificateCreator.isKeyEncrypted(childCert.certificateKeyPath()))
|
||||
assertThat(certificateCreator.isKeyEncrypted(requireNonNull(childCert.certificateKeyPath())))
|
||||
.withFailMessage(ERR_NOT_ENCRYPTED)
|
||||
.isTrue();
|
||||
}
|
||||
|
@ -105,20 +105,23 @@ class TestOpenSSLService {
|
|||
|
||||
var request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImportCA.pem"));
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getSubject().getCommonName()).isEqualTo("test.home");
|
||||
assertThat(request.getSubject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.getSubject().getState()).isEqualTo("SH");
|
||||
assertThat(request.getSubject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.getExtension()).isNull();
|
||||
assertThat(request.subject().getCommonName()).isEqualTo("test.home");
|
||||
assertThat(request.subject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.subject().getState()).isEqualTo("SH");
|
||||
assertThat(request.subject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
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"));
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getSubject().getCommonName()).isEqualTo("test.local");
|
||||
assertThat(request.getSubject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.getSubject().getState()).isEqualTo("SH");
|
||||
assertThat(request.getSubject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.getExtension().getAlternativeDnsNames()).containsExactly("test2.local", "test3.local");
|
||||
assertThat(request.subject().getCommonName()).isEqualTo("test.local");
|
||||
assertThat(request.subject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.subject().getState()).isEqualTo("SH");
|
||||
assertThat(request.subject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.extensions().getFirst().getAlternativeDnsNames())
|
||||
.containsExactly("test2.local", "test3.local");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import de.mlessmann.certassist.models.Certificate;
|
|||
import de.mlessmann.certassist.models.CertificateExtension;
|
||||
import de.mlessmann.certassist.models.CertificateType;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -33,7 +34,8 @@ class CertificateRepositoryTest {
|
|||
certificate.setSubjectCommonName("test-cn");
|
||||
certificate.setType(CertificateType.SIGNED_CERT);
|
||||
certificate.setRequestedKeyLength(1);
|
||||
certificate.setRequestedValidityDays(1);
|
||||
certificate.setNotBefore(OffsetDateTime.now());
|
||||
certificate.setNotAfter(OffsetDateTime.now().plusDays(1));
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue