src/main/kotlin/nl/nuts/discovery/service/CertificatesApiServiceImpl.kt
/*
* Nuts discovery service for Corda network creation
* Copyright (C) 2020 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
package nl.nuts.discovery.service
import net.corda.nodeapi.internal.crypto.X509Utilities
import nl.nuts.discovery.DiscoveryException
import nl.nuts.discovery.api.CertificatesApiService
import nl.nuts.discovery.model.CertificateSigningRequest
import nl.nuts.discovery.model.CertificateWithChain
import nl.nuts.discovery.store.CertificateRepository
import nl.nuts.discovery.store.CustomCASerialRepository
import nl.nuts.discovery.store.NutsCertificateRequestRepository
import nl.nuts.discovery.store.entity.Certificate
import nl.nuts.discovery.store.entity.NutsCertificateRequest
import nl.nuts.discovery.store.entity.PartyId
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1String
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.X509KeyUsage
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject
import org.bouncycastle.util.io.pem.PemReader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.io.ByteArrayInputStream
import java.io.Reader
import java.math.BigInteger
import java.nio.file.Files
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*
import java.util.concurrent.TimeUnit
import javax.security.auth.x500.X500Principal
@Service
class CertificatesApiServiceImpl : AbstractCertificatesService(), CertificatesApiService, CertificateSigningService {
val logger: Logger = LoggerFactory.getLogger(this.javaClass)
private val contentSignerBuilder = JcaContentSignerBuilder("SHA384WITHECDSA")
private val certificateFactory = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME)
@Autowired
lateinit var nutsDiscoveryProperties: NutsDiscoveryProperties
@Autowired
lateinit var nutsCertificateRequestRepository: NutsCertificateRequestRepository
@Autowired
lateinit var certificateRepository: CertificateRepository
@Autowired
lateinit var cASerialRepository: CustomCASerialRepository
override fun listCertificates(otherName: String): List<CertificateWithChain> {
return certificateRepository.findByOid(PartyId.parse(otherName)).map {
CertificateWithChain(it.toPem(), CertificateChain.fromSinglePEM(it.chain).pemEncodedCertificates)
}
}
override fun listRequests(otherName: String): List<CertificateSigningRequest> {
return nutsCertificateRequestRepository.findByOid(PartyId.parse(otherName)).map {
entityToApiModel(it)
}
}
override fun submit(body: String): CertificateSigningRequest {
val nutsCertificateRequest = NutsCertificateRequest.fromPEM(body)
logger.info("Received request for: ${nutsCertificateRequest.name}, oid: ${nutsCertificateRequest.oid}")
nutsCertificateRequestRepository.save(nutsCertificateRequest)
if (nutsDiscoveryProperties.autoAck) {
logger.warn("Signing CSR for: ${nutsCertificateRequest.name}, oid: ${nutsCertificateRequest.oid} automatically")
sign(nutsCertificateRequest)
}
return entityToApiModel(nutsCertificateRequest)
}
private fun entityToApiModel(nutsCertificateRequest: NutsCertificateRequest): CertificateSigningRequest {
return CertificateSigningRequest(
subject = nutsCertificateRequest.name!!,
pem = nutsCertificateRequest.pem!!,
submittedAt = nutsCertificateRequest.submittedAt.toString()
)
}
override fun sign(request: NutsCertificateRequest): X509Certificate {
// some checks
// todo additional checks?
if (request.oid == null) {
throw DiscoveryException("missing oid")
}
logger.info("Signing CSR for: ${request.name}, oid: ${request.oid}")
// convert
val pkcs10 = JcaPKCS10CertificationRequest(request.toPKCS10())
// x509 builder
val issuer = caCertificate()
val certificateBuilder = certificateBuilderWithDefaults(pkcs10, issuer)
addSubjectAltNames(certificateBuilder, request.oid!!, pkcs10)
// signature
val pKey = caKey()
val sigGen = contentSignerBuilder.build(pKey)
val holder: X509CertificateHolder = certificateBuilder.build(sigGen)
val x509CertificateStructure = holder.toASN1Structure()
// Read Certificate into x509 structure
var theCert: X509Certificate? = null
ByteArrayInputStream(x509CertificateStructure.encoded).use {
theCert = certificateFactory.generateCertificate(it) as X509Certificate
certificateRepository.save(Certificate.fromX509Certificate(theCert!!, issuer.subjectDN.name, chain()))
nutsCertificateRequestRepository.delete(request)
}
return theCert!!
}
private fun addSubjectAltNames(builder: X509v3CertificateBuilder, party: PartyId, pkcs10: JcaPKCS10CertificationRequest) {
val names = mutableListOf(GeneralName(GeneralName.otherName, OtherName(ASN1ObjectIdentifier(party.oid), DERUTF8String(party.value))))
val emailAttr = pkcs10.getAttributes(BCStyle.EmailAddress)
if (emailAttr != null && emailAttr.isNotEmpty()) {
val emailASN1String = emailAttr.first().attrValues.getObjectAt(0) as ASN1String
val email = emailASN1String.string
names.add(GeneralName(GeneralName.rfc822Name, email))
}
val subjectAltNames = GeneralNames(names.toTypedArray())
builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames)
}
private fun generateSerial(subject: String): BigInteger {
val caSerial = cASerialRepository.findOrCreate(subject)
val certCount = certificateRepository.countByCa(subject)
val md: MessageDigest = MessageDigest.getInstance("SHA-1")
val text = "${caSerial.salt}${certCount}"
md.update(text.toByteArray(Charsets.UTF_8))
val digest: ByteArray = md.digest()
return BigInteger(digest)
}
private fun certificateBuilderWithDefaults(pkcs10: JcaPKCS10CertificationRequest, issuer: X509Certificate): X509v3CertificateBuilder {
val validity = TimeUnit.DAYS.toMillis(nutsDiscoveryProperties.certificateValidityInDays.toLong())
val issuerSubject = issuer.subjectX500Principal.getName(X500Principal.RFC1779)
return JcaX509v3CertificateBuilder(
X500Name(issuerSubject),
generateSerial(issuerSubject),
Date(System.currentTimeMillis()),
Date(System.currentTimeMillis() + validity),
pkcs10.subject,
pkcs10.publicKey
).addExtension(
Extension.basicConstraints,
true,
BasicConstraints(true) // isCa
).addExtension(
Extension.keyUsage,
true,
X509KeyUsage(
X509KeyUsage.digitalSignature or
X509KeyUsage.keyCertSign or
X509KeyUsage.cRLSign
)
)
}
private fun chain(): String {
val rootPath = loadResourceWithNullCheck(nutsDiscoveryProperties.nutsRootCertPath)
val caPath = loadResourceWithNullCheck(nutsDiscoveryProperties.nutsCACertPath)
return CertificateChain.fromPaths(arrayOf(caPath, rootPath)).asSinglePEM()
}
/**
* returns the Nuts CA certificate
*/
fun caCertificate(): X509Certificate {
return X509Utilities.loadCertificateFromPEMFile(loadResourceWithNullCheck(nutsDiscoveryProperties.nutsCACertPath))
}
/**
* Reads the key from disk and returns a PrivateKey instance, expects PKCS8 encoded EC key
*/
fun caKey(): PrivateKey {
var key: PemObject? = null
PemReader(Files.newBufferedReader(loadResourceWithNullCheck(nutsDiscoveryProperties.nutsCAKeyPath)) as Reader?).use {
key = it.readPemObject()
}
val kf = KeyFactory.getInstance("EC")
return kf.generatePrivate(PKCS8EncodedKeySpec(key!!.content))
}
}