Skip to content

Commit

Permalink
Added renewal support
Browse files Browse the repository at this point in the history
  • Loading branch information
wushilin committed Oct 12, 2023
1 parent 83d99d9 commit 4808c81
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 8 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ Authorization: xxx
Requires viewer role
```

Renew certificate by X days
```
POST /ca/<ca-id>/cert/<cert-id>/<days>
Authorization: xxx
---
Requires admin role
```

Download everything about the cert in a zip file, include cert, csr,
private key in PEM format, jks and pkcs12 keystore, jks truststore,
ca cert in PEM, and all keystore passwords
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = "net.wushilin.minica"
version = "1.0.1-RELEASE"
version = "1.0.2-RELEASE"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/net/wushilin/minica/openssl/CA.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ class CA(var base: File) {
return "CA:$_subject@${base.name}"
}


fun removeCertById(id:String):Cert {
val result = getCertById(id)
val deleteResult = result.base.deleteRecursively()
Expand Down
23 changes: 19 additions & 4 deletions src/main/kotlin/net/wushilin/minica/openssl/Cert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.wushilin.minica.openssl
import net.wushilin.minica.IO
import java.io.File
import java.lang.IllegalArgumentException
import java.nio.charset.StandardCharsets
import java.util.*

class Cert(var base: File){
Expand All @@ -18,6 +19,10 @@ class Cert(var base: File){
val keyFile: File
get() = _keyFile

private val _csrFile:File = File(base, "cert.csr")
val csrFile:File
get() = _csrFile

private var _certFile: File = File(base, "cert.pem")
val certFile: File
get() = _certFile
Expand All @@ -26,6 +31,11 @@ class Cert(var base: File){
val commonName: String
get() = _commonName

fun readPassword():String {
val passwordFile = File(this.base, "password.txt")
val password = passwordFile.readText(StandardCharsets.UTF_8)
return password
}
private var _city: String = ""
val city: String
get() = _city
Expand Down Expand Up @@ -82,10 +92,7 @@ class Cert(var base: File){
_key = IO.readFileAsString(keyFile)
_cert = IO.readFileAsString(certFile)

val props = Properties()
File(base, "meta.properties").inputStream().use {
props.load(it)
}
val props = readMeta()
_validDays = props.getProperty("validDays", "0").toInt()
this._city = props.getProperty("city", "")
this._commonName = props.getProperty("commonName", "")
Expand All @@ -105,6 +112,14 @@ class Cert(var base: File){
}
}

fun readMeta():Properties {
val props = Properties()
File(base, "meta.properties").inputStream().use {
props.load(it)
}
return props
}

override fun toString():String {
return "CERT:$_subject@${base.name}"
}
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/net/wushilin/minica/rest/CARestService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ class CARestService {
}
}

@PostMapping("/ca/{caid}/cert/{certid}/renew/{days}")
fun renewCert(@PathVariable("caid") caid: String, @PathVariable("certid") certid:String, @PathVariable("days") days:Int):Cert {
try {
val ca = caSvc.getCAById(caid)
val cert = ca.getCertById(certid)
// both exists, good!
return caSvc.renewCert(ca, cert, days)
} catch(ex:Exception) {
log.error("Failed to extend cert by id $caid/$certid for $days days", ex)
throw ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "entity error"
)
}
}
@GetMapping("/ca/{caid}/cert/{certid}")
fun getCACert(@PathVariable("caid") caid: String, @PathVariable("certid") certid: String): Cert {
try {
Expand Down
129 changes: 129 additions & 0 deletions src/main/kotlin/net/wushilin/minica/services/CAService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,135 @@ class CAService {
)
}

fun renewCert(ca:CA, cert:Cert, days:Int):Cert {
var caBase = ca.base
val certBase = cert.base
if(days < 1 || days > 7350) {
throw IllegalArgumentException("Invalid days. 1~7350 only");
}
try {
val processResult2 = Run.ExecWait(
caBase, 60000, null, listOf(
config.opensslPath,
"ca",
"-config",
"openssl-ca.conf",
"-days",
"$days",
"-batch",
"-policy",
"signing_policy",
"-extensions",
"signing_req",
"-out",
"$certBase/cert.renew.pem",
"-infiles",
"$certBase/cert.csr"
)
)
if (!processResult2.isSuccessful()) {
// Cleanup is caller's responsibility
log.error("Failed to sign CSR: ${processResult2}")
throw IllegalArgumentException("Failed to sign CSR : ${processResult2.error()}")
}
log.info("Renewed cert into $certBase/cert.renew.pem")
val src = File("$certBase/cert.renew.pem")
val dest = File("$certBase/cert.pem")
val renameResult = src.renameTo(dest)
if(renameResult) {
log.info("Renamed $src to $dest")
} else {
log.error("Unable to rename $src to $dest")
throw IllegalArgumentException("Failed to sign CSR and rename new cert file!")
}

val random = cert.readPassword()
val pkcs12File = File("$certBase/cert.p12")
if(!pkcs12File.deleteRecursively()) {
log.error("Can't delete pkcs12 file $pkcs12File");
}
// openssl pkcs12 -export -out Cert.p12 -in cert.pem -inkey key.pem -passin pass:root -passout pass:root
val processResult3 = Run.ExecWait(
caBase, 60000, null, listOf(
config.opensslPath, "pkcs12", "-export", "-out", "$certBase/cert.p12",
"-in", "$certBase/cert.pem", "-inkey", "$certBase/cert.key", "-passout", "pass:$random"
)
)
if (!processResult3.isSuccessful()) {
log.error("Failed to convert to PKCS12: ${processResult3}")
throw IllegalArgumentException("Failed to convert to PKCS12 : ${processResult3.error()}")
}

FileOutputStream("$certBase/password.txt").use {
it.write(random.toByteArray())
}

val jksFile = File("$certBase/cert.jks")
if(!jksFile.deleteRecursively()) {
log.error("Failed to delete JKS file $jksFile")
}

//keytool -importkeystore -srcstorepass changeme -srckeystore $outdir/$CN.p12 -srcstoretype pkcs12 -destkeystore $outdir/$CN.jks -deststoretype jks -deststorepass changeme
val processResult4 = Run.ExecWait(
certBase, 60000, null, listOf(
config.keytoolPath,
"-importkeystore",
"-srcstorepass",
"$random",
"-srckeystore",
"cert.p12",
"-srcstoretype",
"pkcs12",
"-destkeystore",
"cert.jks",
"-deststoretype",
"jks",
"-deststorepass",
"$random"
)
)
if (!processResult4.isSuccessful()) {
log.error("Failed to convert to JKS: ${processResult3}")
throw IllegalArgumentException("Failed to convert to JKS : ${processResult4.error()}")
}

//keytool -import -v -trustcacerts -alias server-alias
//-file server.cer -keystore cacerts.jks -keypass changeit -storepass changeit
val meta = cert.readMeta()
meta.put("issueTime", "${System.currentTimeMillis()}")
meta.put("validDays", "$days")
FileOutputStream(File(certBase, "meta.properties")).use {
meta.store(it, "Generated by MiniCA")
}
File(certBase, "CERT.complete").outputStream().use {
it.write("Done!".toByteArray())
}
log.info("Renewed cert ${cert.subject} ($certBase) in $ca")
createBundle(
certBase,
File(certBase, "bundle.zip"),
listOf(
"cert.csr",
"cert.jks",
"cert.key",
"cert.p12",
"cert.pem",
"meta.properties",
"password.txt=>cert-jks-password.txt",
"password.txt=>cert-p12-password.txt",
"../ca-cert.pem=>ca.pem",
"../truststore.jks",
"../password.txt=>truststore-jks-password.txt"
)
)
return ca.getCertById(certBase.name)
// must be successfully now
} finally {
// delete cert.renew.pem
val toDelete = File("$certBase/cert.renew.pem")
toDelete.deleteRecursively()
}
}
fun createCert(
ca: CA,
commonName: String,
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
<style>.mat-typography{font:400 14px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-typography{font:400 14px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}body{margin:50;font-family:Roboto,Helvetica Neue,sans-serif}html,body{height:100%}body{margin:0;font-family:Roboto,Helvetica Neue,sans-serif}</style><link rel="stylesheet" href="styles.2f90291d572fac38.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.2f90291d572fac38.css"></noscript></head>
<body class="mat-typography">
<app-root></app-root>
<script src="runtime.3e80a074dc94328a.js" type="module"></script><script src="polyfills.ea1d76ecbfa0f129.js" type="module"></script><script src="main.1687d7a6e66c24af.js" type="module"></script>
<script src="runtime.3e80a074dc94328a.js" type="module"></script><script src="polyfills.ea1d76ecbfa0f129.js" type="module"></script><script src="main.dfff79e842c0147f.js" type="module"></script>

</body></html>
1 change: 0 additions & 1 deletion src/main/resources/static/main.1687d7a6e66c24af.js

This file was deleted.

1 change: 1 addition & 0 deletions src/main/resources/static/main.dfff79e842c0147f.js

Large diffs are not rendered by default.

0 comments on commit 4808c81

Please sign in to comment.