From 533dd51a5b19c358646f6ed165a0bc7d32535309 Mon Sep 17 00:00:00 2001 From: Matthias Griebl Date: Tue, 26 Mar 2024 13:31:50 +0100 Subject: [PATCH 1/7] Add EST identity renew endpoint --- .../fhg/aisec/ids/webconsole/api/CertApi.kt | 127 ++++++++++++++++++ .../webconsole/api/data/EstIdRenewRequest.kt | 26 ++++ 2 files changed, 153 insertions(+) create mode 100644 ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/data/EstIdRenewRequest.kt diff --git a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt index 58374797..a8d95144 100644 --- a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt +++ b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt @@ -26,6 +26,7 @@ import de.fhg.aisec.ids.api.acme.AcmeTermsOfService import de.fhg.aisec.ids.api.settings.Settings import de.fhg.aisec.ids.webconsole.ApiController import de.fhg.aisec.ids.webconsole.api.data.Cert +import de.fhg.aisec.ids.webconsole.api.data.EstIdRenewRequest import de.fhg.aisec.ids.webconsole.api.data.EstIdRequest import de.fhg.aisec.ids.webconsole.api.data.Identity import de.fhg.aisec.ids.webconsole.api.helper.ProcessExecutor @@ -41,6 +42,7 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess import io.ktor.serialization.jackson.jackson import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -82,7 +84,9 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.Base64 import java.util.UUID +import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext +import javax.net.ssl.SSLParameters import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import javax.security.auth.x500.X500Principal @@ -407,6 +411,63 @@ class CertApi( } } + @PostMapping("/renew_est_identity", consumes = [MediaType.APPLICATION_JSON]) + @ApiOperation("Renew a certificate from an EST") + @ApiResponses( + ApiResponse(code = 200, message = "Successfully renewed the certificate"), + ApiResponse(code = 500, message = "Error renewing certificate") + ) + suspend fun renewEstIdentities( + @RequestBody req: EstIdRenewRequest + ) { + LOG.debug("Start renewing EST certificate.") + + val keyStoreFile = getKeystoreFile(settings.connectorConfig.keystoreName) + val keyStore = + KeyStore.getInstance("pkcs12") + .also { it.load(keyStoreFile.inputStream(), KEYSTORE_PWD.toCharArray()) } + + val oldKey = keyStore.getKey(req.alias, KEYSTORE_PWD.toCharArray()) as PrivateKey + val oldCert = keyStore.getCertificate(req.alias) as X509Certificate + + LOG.debug("Fetching root certificates from EST...") + val caCerts = fetchEstCaCerts(req.estUrl, req.rootCertHash) + + caCerts.firstOrNull { it.verify(it) }?.let { + LOG.debug("Storing root CA certificate in TrustStore...") + storeCertificate(settings.connectorConfig.truststoreName, listOf(it)) + } ?: LOG.warn("No (valid) root CA certificate has been found. EST process may fail!") + + LOG.debug("Generating new keys...") + val keys = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.generateKeyPair() + + LOG.debug("Generating CSR...") + val csr = generatePKCS10(keys) + + LOG.debug("Sending EST renew request...") + val pkcs7 = sendEstIdRenewReq(req, csr, oldKey, oldCert) + + pkcs7.certificates.firstOrNull { it.publicKey == keys.public }?.let { + LOG.debug("Found EST certificate, assembling certificate chain...") + val certificateChain = mutableListOf(it) + var lastCertificate = it + // The last certificate (root) is self-signed + while (!lastCertificate.verify(lastCertificate)) { + // Find CA certificate signing last element of chain + caCerts.firstOrNull { ca -> lastCertificate.verify(ca) }?.let { nextCa -> + certificateChain += nextCa + lastCertificate = nextCa + } ?: throw RuntimeException( + "Could not create certificate chain, " + + "did not find signer for this certificate:\n$lastCertificate" + ) + } + LOG.debug("Storing EST certificate (full chain) using alias \"{}\"...", req.alias) + storeCertificate(settings.connectorConfig.keystoreName, certificateChain, keys.private, req.alias) + LOG.debug("EST re-enrollment completed successfully!") + } + } + @Throws(java.lang.Exception::class) private fun generatePKCS10(keys: KeyPair): ByteArray { val sigAlg = "SHA256WithRSA" @@ -481,6 +542,72 @@ class CertApi( return PKCS7(encoded) } + private suspend fun sendEstIdRenewReq( + req: EstIdRenewRequest, + csr: ByteArray, + clientKey: PrivateKey, + clientCert: X509Certificate + ): PKCS7 { + val trustStoreFile = getKeystoreFile(settings.connectorConfig.truststoreName) + val trustManagers = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).also { tmf -> + KeyStore.getInstance("pkcs12").also { + FileInputStream(trustStoreFile).use { fis -> + it.load(fis, KEYSTORE_PWD.toCharArray()) + tmf.init(it) + } + } + }.trustManagers + val keyStore = KeyStore.getInstance("pkcs12") + // This does not perform IO since this load creates a new keystore instance + @Suppress("BlockingMethodInNonBlockingContext") + keyStore.load(null) + keyStore.setKeyEntry("1", clientKey, "".toCharArray(), arrayOf(clientCert)) + val keyManagers = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).also { + it.init(keyStore, null) + }.keyManagers + val secureHttpClient = + HttpClient(Java) { + engine { + config { + sslContext( + SSLContext.getInstance("TLS").apply { + init(keyManagers, trustManagers, null) + } + ) + sslParameters( + SSLParameters().apply { + needClientAuth = true + } + ) + } + } + install(ContentNegotiation) { + jackson() + } + } + + val url = "${req.estUrl}/.well-known/est/simplereenroll" + val csrString = String(csr, StandardCharsets.UTF_8).replace(CLEAR_PEM_REGEX, "") + val resp = + secureHttpClient.post(url) { + setBody(csrString) + headers { + append("Content-Type", "application/pkcs10") + append("Content-Transfer-Encoding", "base64") + } + } + + if (!resp.status.isSuccess()) { + throw RuntimeException("Failed to fetch renewed certificate: ${resp.status}\n${resp.bodyAsText()}") + } + + val res = resp.bodyAsText() + val encoded = Base64.getDecoder().decode(res.replace(WHITESPACE_REGEX, "")) + return PKCS7(encoded) + } + private fun storeCertificate( storeFilename: String, certificateChain: List, diff --git a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/data/EstIdRenewRequest.kt b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/data/EstIdRenewRequest.kt new file mode 100644 index 00000000..f596c60f --- /dev/null +++ b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/data/EstIdRenewRequest.kt @@ -0,0 +1,26 @@ +/*- + * ========================LICENSE_START================================= + * ids-webconsole + * %% + * Copyright (C) 2024 Fraunhofer AISEC + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package de.fhg.aisec.ids.webconsole.api.data + +data class EstIdRenewRequest( + val estUrl: String, + val rootCertHash: String, + val alias: String +) From cc53cfe39aeddf3e40c4efc58339b94b30b91371 Mon Sep 17 00:00:00 2001 From: Matthias Griebl Date: Thu, 28 Mar 2024 17:07:28 +0100 Subject: [PATCH 2/7] Add identity renewal to the web console --- .../src/main/angular/src/app/app.module.ts | 4 ++- .../src/main/angular/src/app/app.routing.ts | 4 ++- .../keycerts/certificate-card.component.html | 7 +++-- .../keycerts/certificate-card.component.ts | 8 +++++ .../keycerts/est-re-enrollment.interface.ts | 5 ++++ .../angular/src/app/keycerts/est-service.ts | 9 ++++++ .../keycerts/identityrenewest.component.html | 29 +++++++++++++++++++ .../keycerts/identityrenewest.component.ts | 27 +++++++++++++++++ .../src/app/keycerts/keycerts.component.html | 2 +- .../src/app/keycerts/keycerts.component.ts | 9 +++++- 10 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/est-re-enrollment.interface.ts create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts diff --git a/ids-webconsole/src/main/angular/src/app/app.module.ts b/ids-webconsole/src/main/angular/src/app/app.module.ts index 72940b65..44ae33d4 100644 --- a/ids-webconsole/src/main/angular/src/app/app.module.ts +++ b/ids-webconsole/src/main/angular/src/app/app.module.ts @@ -54,6 +54,7 @@ import { UserService } from './users/user.service'; import { UserCardComponent } from './users/user-card.component'; import { NewIdentityESTComponent } from './keycerts/identitynewest.component'; import { ESTService } from './keycerts/est-service'; +import { RenewIdentityESTComponent } from './keycerts/identityrenewest.component'; @NgModule({ imports: [ @@ -101,7 +102,8 @@ import { ESTService } from './keycerts/est-service'; DetailUserComponent, UserCardComponent, UsersComponent, - NewIdentityESTComponent + NewIdentityESTComponent, + RenewIdentityESTComponent ], providers: [ HTTP_PROVIDER, diff --git a/ids-webconsole/src/main/angular/src/app/app.routing.ts b/ids-webconsole/src/main/angular/src/app/app.routing.ts index 951b2eda..d1548de9 100644 --- a/ids-webconsole/src/main/angular/src/app/app.routing.ts +++ b/ids-webconsole/src/main/angular/src/app/app.routing.ts @@ -18,6 +18,7 @@ import { RouteeditorComponent } from './routes/routeeditor/routeeditor.component import { UsersComponent } from './users/users.component'; import { NewUserComponent } from './users/usernew.component'; import { DetailUserComponent } from './users/userdetail.component'; +import { RenewIdentityESTComponent } from './keycerts/identityrenewest.component'; import { RoutesComponent } from './routes/routes.component'; import { NewIdentityESTComponent } from './keycerts/identitynewest.component'; @@ -42,7 +43,8 @@ const appRoutes: Routes = [ { path: 'usernew', component: NewUserComponent, canActivate: [AuthGuard] }, { path: 'userdetail', component: DetailUserComponent, canActivate: [AuthGuard] }, { path: 'certificates', component: KeycertsComponent, canActivate: [AuthGuard] }, - { path: 'identitynewest', component: NewIdentityESTComponent, canActivate: [AuthGuard] } + { path: 'identitynewest', component: NewIdentityESTComponent, canActivate: [AuthGuard] }, + { path: 'identityrenewest/:alias', component: RenewIdentityESTComponent, canActivate: [AuthGuard] } ] }, // Pages using the "login" layout (centered full page without sidebar) diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.html b/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.html index 2f204320..05639c9b 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.html +++ b/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.html @@ -7,10 +7,13 @@ {{ certificate.subjectDistinguishedName }} - + diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.ts b/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.ts index 5f463eaa..7077411e 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.ts +++ b/ids-webconsole/src/main/angular/src/app/keycerts/certificate-card.component.ts @@ -16,6 +16,7 @@ export class CertificateCardComponent implements OnInit { @Input() public certificates: Certificate[]; @Input() public trusts: Certificate[]; @Input() private readonly onDeleteCallback: (alias: string) => void; + @Input() private readonly onRenewCallback: (alias: string) => void = null; public result: string; constructor(private readonly confirmService: ConfirmService) { @@ -29,6 +30,13 @@ export class CertificateCardComponent implements OnInit { return item.subjectS + item.subjectCN + item.subjectOU + item.subjectO + item.subjectL + item.subjectC; } + public onRenew(alias: string): void { + // Sanity check + if (this.onRenewCallback) { + this.onRenewCallback(alias); + } + } + public async onDelete(alias: string): Promise { return this.confirmService.activate('Are you sure that you want to delete this item?') .then(res => { diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/est-re-enrollment.interface.ts b/ids-webconsole/src/main/angular/src/app/keycerts/est-re-enrollment.interface.ts new file mode 100644 index 00000000..1f993815 --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/est-re-enrollment.interface.ts @@ -0,0 +1,5 @@ +export interface EstReEnrollment { + estUrl: string; + rootCertHash: string; + alias: string; +}; diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/est-service.ts b/ids-webconsole/src/main/angular/src/app/keycerts/est-service.ts index 24b06b33..75967c41 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/est-service.ts +++ b/ids-webconsole/src/main/angular/src/app/keycerts/est-service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; import { EstEnrollment } from './est-enrollment.interface'; +import { EstReEnrollment } from './est-re-enrollment.interface'; @Injectable() export class ESTService { @@ -40,4 +41,12 @@ export class ESTService { responseType: 'text' }); } + + // Renew an existing identity identified by its alias via the EST + public renewIdentity(data: EstReEnrollment) { + return this.http.post(environment.apiURL + '/certs/renew_est_identity', data, { + headers: new HttpHeaders({'Content-Type': 'application/json'}), + responseType: 'text' + }); + } } diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html new file mode 100644 index 00000000..f269507d --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html @@ -0,0 +1,29 @@ +
+
+
+

Renew Identity

+
+
+
+
+
EST Re-Enrollment
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts new file mode 100644 index 00000000..3bc571e8 --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { ESTService } from './est-service'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + templateUrl: './identityrenewest.component.html' +}) +export class RenewIdentityESTComponent { + estUrl = 'https://daps-dev.aisec.fraunhofer.de'; + rootCertHash = '7d3f260abb4b0bfa339c159398c0ab480a251faa385639218198adcad9a3c17d'; + + constructor(private readonly titleService: Title, + private readonly estService: ESTService, + private readonly router: Router, + private readonly route: ActivatedRoute) { + this.titleService.setTitle('Renew Identity via the EST'); + } + + onSubmit() { + this.estService.renewIdentity({ + estUrl: this.estUrl, + rootCertHash: this.rootCertHash, + alias: this.route.snapshot.paramMap.get('alias') + }).subscribe(() => this.router.navigate([ '/certificates' ])); + } +} diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/keycerts.component.html b/ids-webconsole/src/main/angular/src/app/keycerts/keycerts.component.html index 90166399..1aeb1b11 100755 --- a/ids-webconsole/src/main/angular/src/app/keycerts/keycerts.component.html +++ b/ids-webconsole/src/main/angular/src/app/keycerts/keycerts.component.html @@ -5,7 +5,7 @@

My Identities

- +
+ +
+
+ {{ error }} + + Check the trusted connector log for more details + +
+
\ No newline at end of file diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts index 3bc571e8..94e86cb4 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts +++ b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ESTService } from './est-service'; import { ActivatedRoute, Router } from '@angular/router'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ templateUrl: './identityrenewest.component.html' @@ -9,6 +10,7 @@ import { ActivatedRoute, Router } from '@angular/router'; export class RenewIdentityESTComponent { estUrl = 'https://daps-dev.aisec.fraunhofer.de'; rootCertHash = '7d3f260abb4b0bfa339c159398c0ab480a251faa385639218198adcad9a3c17d'; + error = null; constructor(private readonly titleService: Title, private readonly estService: ESTService, @@ -17,11 +19,28 @@ export class RenewIdentityESTComponent { this.titleService.setTitle('Renew Identity via the EST'); } + handleError(err: HttpErrorResponse) { + if (err.status === 0) { + this.error = 'Network Error'; + } else { + const errObj = JSON.parse(err.error); + if (errObj.message) { + this.error = errObj.message; + } else { + // Errors have no message if it is disabled by the spring application + this.error = `Error response from connector: ${err.status}: ${errObj.error}`; + } + } + } + onSubmit() { this.estService.renewIdentity({ estUrl: this.estUrl, rootCertHash: this.rootCertHash, alias: this.route.snapshot.paramMap.get('alias') - }).subscribe(() => this.router.navigate([ '/certificates' ])); + }).subscribe( + () => this.router.navigate([ '/certificates' ]), + err => this.handleError(err) + ); } } diff --git a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt index a8d95144..ef689ae3 100644 --- a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt +++ b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt @@ -415,6 +415,7 @@ class CertApi( @ApiOperation("Renew a certificate from an EST") @ApiResponses( ApiResponse(code = 200, message = "Successfully renewed the certificate"), + ApiResponse(code = 400, message = "Error response from the EST"), ApiResponse(code = 500, message = "Error renewing certificate") ) suspend fun renewEstIdentities( @@ -600,7 +601,10 @@ class CertApi( } if (!resp.status.isSuccess()) { - throw RuntimeException("Failed to fetch renewed certificate: ${resp.status}\n${resp.bodyAsText()}") + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Failed to fetch renewed certificate from EST: ${resp.bodyAsText()}" + ) } val res = resp.bodyAsText() From 6ac49b6ac6b2ce1620e6f6c2f91caedbb9e4dba1 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Fri, 14 Jun 2024 11:26:35 +0200 Subject: [PATCH 4/7] Code style fixes --- .../fhg/aisec/ids/webconsole/api/CertApi.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt index 98ceb385..73576ec3 100644 --- a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt +++ b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt @@ -427,7 +427,8 @@ class CertApi( val keyStoreFile = getKeystoreFile(settings.connectorConfig.keystoreName) val keyStore = - KeyStore.getInstance("pkcs12") + KeyStore + .getInstance("pkcs12") .also { it.load(keyStoreFile.inputStream(), KEYSTORE_PWD.toCharArray()) } val oldKey = keyStore.getKey(req.alias, KEYSTORE_PWD.toCharArray()) as PrivateKey @@ -555,23 +556,27 @@ class CertApi( ): PKCS7 { val trustStoreFile = getKeystoreFile(settings.connectorConfig.truststoreName) val trustManagers = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).also { tmf -> - KeyStore.getInstance("pkcs12").also { - FileInputStream(trustStoreFile).use { fis -> - it.load(fis, KEYSTORE_PWD.toCharArray()) - tmf.init(it) + TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()) + .also { tmf -> + KeyStore.getInstance("pkcs12").also { + FileInputStream(trustStoreFile).use { fis -> + it.load(fis, KEYSTORE_PWD.toCharArray()) + tmf.init(it) + } } - } - }.trustManagers + }.trustManagers val keyStore = KeyStore.getInstance("pkcs12") // This does not perform IO since this load creates a new keystore instance @Suppress("BlockingMethodInNonBlockingContext") keyStore.load(null) keyStore.setKeyEntry("1", clientKey, "".toCharArray(), arrayOf(clientCert)) val keyManagers = - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).also { - it.init(keyStore, null) - }.keyManagers + KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()) + .also { + it.init(keyStore, null) + }.keyManagers val secureHttpClient = HttpClient(Java) { engine { From 084776df02807d88da040ffecb49303fac407f5d Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Fri, 14 Jun 2024 11:48:37 +0200 Subject: [PATCH 5/7] Minor spelling improvements --- .../main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt index 73576ec3..1c7d0240 100644 --- a/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt +++ b/ids-webconsole/src/main/kotlin/de/fhg/aisec/ids/webconsole/api/CertApi.kt @@ -364,7 +364,7 @@ class CertApi( @PostMapping("/request_est_identity", consumes = [MediaType.APPLICATION_JSON]) @ApiOperation( - value = "Get CA certificate from EST", + value = "Get CA certificate from EST server", notes = "" ) @ApiResponses( @@ -434,7 +434,7 @@ class CertApi( val oldKey = keyStore.getKey(req.alias, KEYSTORE_PWD.toCharArray()) as PrivateKey val oldCert = keyStore.getCertificate(req.alias) as X509Certificate - LOG.debug("Fetching root certificates from EST...") + LOG.debug("Fetching root certificates from EST server...") val caCerts = fetchEstCaCerts(req.estUrl, req.rootCertHash) caCerts.firstOrNull { it.verify(it) }?.let { @@ -612,7 +612,7 @@ class CertApi( if (!resp.status.isSuccess()) { throw ResponseStatusException( HttpStatus.BAD_REQUEST, - "Failed to fetch renewed certificate from EST: ${resp.bodyAsText()}" + "Failed to fetch renewed certificate from EST server: ${resp.bodyAsText()}" ) } From e6d0fc0ee238b25c3ff71d31e7e0955bcb788a36 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Fri, 14 Jun 2024 13:03:24 +0200 Subject: [PATCH 6/7] Bumped version and created example ZIP for release --- build.gradle.kts | 2 +- examples/trusted-connector-examples_7.3.0.zip | Bin 0 -> 48342 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 examples/trusted-connector-examples_7.3.0.zip diff --git a/build.gradle.kts b/build.gradle.kts index 2dd2e80f..0c0360e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,7 +26,7 @@ licenseReport { allprojects { group = "de.fhg.aisec.ids" - version = "7.2.2" + version = "7.3.0" val versionRegex = ".*((rc|beta|alpha)-?[0-9]*|-b[0-9.]+)$".toRegex(RegexOption.IGNORE_CASE) diff --git a/examples/trusted-connector-examples_7.3.0.zip b/examples/trusted-connector-examples_7.3.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..79e5a78d9c5b7b07f8998f2181a5a35f2efe6f83 GIT binary patch literal 48342 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

Ka4la}K@CG0QPwwI8Cj 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

8DTM>Q=mNn_ z!jSCamENQ6Jvv zoFJ7k*BKNs-mO`(iS#`Z)u-C*dzANBY}omod%9WSSHSMg9*S{xkIZeP&{8cwJ5w9O zF0xw_^$UHoBPRxcZ{LkI6!fOrGKjHei}^QWjpnyop8w8i8TiR*Y5EVRW&9_nMdlw) z3$d^|^>awdj_5+RIW4h5 zJxM)FC2Ry{^l0|UvfK{YiLRP*?8rUh$(%#C3CFaS9g&4as)Ks>;kQ0)bfrk_RP-XD zdAsY1$(LbHlssVc28>&iN-8L(?2)vEOe@q2YCBljq_&iXA}I8+@jIQscE#&K;DF>W@r2$Ejz?0DQV8 zpzN-UFGkdat{ngHSr9!K=$KcuWg2Yrs~H(Jh%1=B^H~tzu1kq1ahSwqW^$TE6#U?` z3@ifmf^|!bR1(By05(0M^b9Og#VbMZxr64V780wiLs-Nc{701G#?K99uA8-?3G}^z zHR|^ppa9iMl?De@5ZDAb5pk8iM_e!pD0%uS*XW$jayTXFGMroe>5GIw+)ODxC+K$9 zieWPK^T-_moWY)RK1*FVe+REG9o_g*${n!GY(@fbDwa4Hqj{oSK3_(TYW;z2Un2_O zt-N5PrBrWtW*%8GJZ=0OwPoZu4zdgYsV}%r55Fo)9%HhbPL~%SHf)~J+Uca^EbW?y zg4=f8`z_Y%8}Li(ULho-G=`vu7>G#$d`^tH5FiBH$kE`I zo~^B=nSq|8nT<8uBdcEkUKG;P#Q(VZ{Q5;{$de~zCO8N9ai(iw^yoiHu&!zWeSi98 z5DiEsm~)UH?)txhKwme3cS#c-?w=sg7q4Gc0y^Q?<))BRyj=dr6%|(jpwog~Rv^Uq zAArkQ%C44&PK|T9Je|pp@|RMHT`dotrsh0>&V}efGS~dHDSwwv=PD9(?v(SiHWxx* z^=Bk#Z^%_d=sXeUIae;E9TM>NC*6L2|GNwI)q= zc^SXZ_21)P?BlQELkBsYM_*pZ2aiAC|McWug@TR~yxg>Io_~S*V^={3h0$H;1$pxE z`U~FQdJ8%{;yjw{LOwnD3*@gIb}6>(YC}L1iZ9o~7Vy8V<nZ>=;SaJ7L5XSPF93h+eQ3_v<=$_4`U}dBK8HFy zKF{lQRWWEz)Va(5Lj0ot1o@qFcohhm+j9A( Date: Fri, 14 Jun 2024 15:42:12 +0200 Subject: [PATCH 7/7] Extract the error snackbar into its onw component It now disappears properly and can be reused for other components. --- .../src/main/angular/src/app/app.module.ts | 4 +++- .../keycerts/identityrenewest.component.html | 10 +-------- .../keycerts/identityrenewest.component.ts | 14 ++++++++----- .../src/app/keycerts/snackbar.component.css | 13 ++++++++++++ .../src/app/keycerts/snackbar.component.html | 9 ++++++++ .../src/app/keycerts/snackbar.component.ts | 21 +++++++++++++++++++ 6 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.css create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.html create mode 100644 ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.ts diff --git a/ids-webconsole/src/main/angular/src/app/app.module.ts b/ids-webconsole/src/main/angular/src/app/app.module.ts index 48662099..1153869d 100644 --- a/ids-webconsole/src/main/angular/src/app/app.module.ts +++ b/ids-webconsole/src/main/angular/src/app/app.module.ts @@ -55,6 +55,7 @@ import { UserCardComponent } from './users/user-card.component'; import { NewIdentityESTComponent } from './keycerts/identitynewest.component'; import { ESTService } from './keycerts/est-service'; import { RenewIdentityESTComponent } from './keycerts/identityrenewest.component'; +import { SnackbarComponent } from './keycerts/snackbar.component'; @NgModule({ declarations: [ AppComponent, @@ -90,7 +91,8 @@ import { RenewIdentityESTComponent } from './keycerts/identityrenewest.component UserCardComponent, UsersComponent, NewIdentityESTComponent, - RenewIdentityESTComponent + RenewIdentityESTComponent, + SnackbarComponent ], bootstrap: [ AppComponent diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html index 2fc0eed1..9014bb8d 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html +++ b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.html @@ -27,12 +27,4 @@

EST Re-Enrollment
-
-
- {{ error }} - - Check the trusted connector log for more details - -
- -
\ No newline at end of file + diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts index 94e86cb4..36f9d33a 100644 --- a/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts +++ b/ids-webconsole/src/main/angular/src/app/keycerts/identityrenewest.component.ts @@ -1,8 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ESTService } from './est-service'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; +import { SnackbarComponent } from './snackbar.component'; @Component({ templateUrl: './identityrenewest.component.html' @@ -10,7 +11,9 @@ import { HttpErrorResponse } from '@angular/common/http'; export class RenewIdentityESTComponent { estUrl = 'https://daps-dev.aisec.fraunhofer.de'; rootCertHash = '7d3f260abb4b0bfa339c159398c0ab480a251faa385639218198adcad9a3c17d'; - error = null; + + @ViewChild("errorSnackbar") + errorSnackbar: SnackbarComponent; constructor(private readonly titleService: Title, private readonly estService: ESTService, @@ -21,16 +24,17 @@ export class RenewIdentityESTComponent { handleError(err: HttpErrorResponse) { if (err.status === 0) { - this.error = 'Network Error'; + this.errorSnackbar.title = 'Network Error'; } else { const errObj = JSON.parse(err.error); if (errObj.message) { - this.error = errObj.message; + this.errorSnackbar.title = errObj.message; } else { // Errors have no message if it is disabled by the spring application - this.error = `Error response from connector: ${err.status}: ${errObj.error}`; + this.errorSnackbar.title = `Error response from connector: ${err.status}: ${errObj.error}`; } } + this.errorSnackbar.visible = true; } onSubmit() { diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.css b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.css new file mode 100644 index 00000000..eecfe1b5 --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.css @@ -0,0 +1,13 @@ +.snackbar { + -webkit-transform: translate(-50%, 100px); + transform: translate(-50%, 100px); +} + +.snackbar-content { + display: flex; + flex-direction: column; +} + +.snackbar-subtitle { + font-size: smaller; +} diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.html b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.html new file mode 100644 index 00000000..4c12ca43 --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.html @@ -0,0 +1,9 @@ +
+
+ {{ title }} + + {{ subtitle }} + +
+ +
diff --git a/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.ts b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.ts new file mode 100644 index 00000000..595724fa --- /dev/null +++ b/ids-webconsole/src/main/angular/src/app/keycerts/snackbar.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'snackbar', + templateUrl: './snackbar.component.html', + styleUrl: './snackbar.component.css' +}) +export class SnackbarComponent { + @Input() title: string = null; + @Input() subtitle: string = null; + @Input() visible: boolean = false; + @Input() onDismiss: ()=>void = null; + + invokeOnDismiss() { + if (this.onDismiss !== null) { + this.onDismiss() + } else { + this.visible = false; + } + } +} \ No newline at end of file