src/main/kotlin/nl/nuts/discovery/service/CertificateAndKeyService.kt
/*
* Nuts discovery service for Corda network creation
* Copyright (C) 2019 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.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.signWithCert
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.toJca
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.SignedNetworkMap
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import nl.nuts.discovery.store.CertificateRepository
import nl.nuts.discovery.store.CertificateRequestRepository
import nl.nuts.discovery.store.entity.Certificate
import nl.nuts.discovery.store.entity.CertificateRequest
import org.bouncycastle.asn1.ASN1String
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemReader
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.io.Reader
import java.nio.file.Files
import java.security.KeyFactory
import java.security.KeyPair
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*
import javax.security.auth.x500.X500Principal
import javax.transaction.Transactional
/**
* Certificate and key logic
*/
//@Profile(value = arrayOf("dev", "test", "default"))
@Service
class CertificateAndKeyService : AbstractCertificatesService() {
@Autowired
lateinit var nutsDiscoveryProperties: NutsDiscoveryProperties
@Autowired
lateinit var certificateRepository: CertificateRepository
@Autowired
lateinit var certificateRequestRepository: CertificateRequestRepository
/**
* Create a certificate, add the email extension with email from the request
* and sign it with the Nuts intermediate keyPair.
*
* Deletes request and stores certificate
*/
@Transactional
fun signCertificate(request: CertificateRequest): X509Certificate {
val pkcs10 = JcaPKCS10CertificationRequest(request.toPKCS10())
val name = CordaX500Name.parse(request.name!!)
val issuerCertificate = intermediateCertificate()
val certificateType = CertificateType.NODE_CA
val issuer = issuerCertificate.subjectX500Principal
val issuerKeyPair = intermediateKeyPair()
val subject = name.x500Principal
val subjectPublicKey = pkcs10.publicKey
val validityWindow = X509Utilities.DEFAULT_VALIDITY_WINDOW
val window = X509Utilities.getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate)
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
val provider = Crypto.findProvider(signatureScheme.providerName)
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
val builder = X509Utilities.createPartialCertificate(
certificateType,
issuer,
issuerKeyPair.public,
subject,
subjectPublicKey,
window,
null, // todo add name constraints which confirm to CA tree
null,
null)
val emailAttr = pkcs10.getAttributes(BCStyle.EmailAddress)!!.first()
val emailASN1String = emailAttr!!.attrValues.getObjectAt(0) as ASN1String
val email = emailASN1String.string
val subjectAltNames = GeneralNames(GeneralName(GeneralName.rfc822Name, email))
builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames)
val x509 = builder.build(signer)
// validate
x509.run {
require(isValidOn(Date())) { "Certificate is not valid at instant now" }
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))) { "Invalid signature" }
}
val x509Jca = x509.toJca()
// delete request and store certificate
certificateRepository.save(Certificate.fromX509Certificate(x509Jca, issuer.getName(X500Principal.RFC1779), chain()))
certificateRequestRepository.delete(request)
return x509Jca
}
private fun chain() : String {
val rootPath = loadResourceWithNullCheck(nutsDiscoveryProperties.cordaRootCertPath)
val caPath = loadResourceWithNullCheck(nutsDiscoveryProperties.intermediateCertPath)
return CertificateChain.fromPaths(arrayOf(caPath, rootPath)).asSinglePEM()
}
/**
* returns the Corda network rootcertificate
*/
fun cordaRootCertificate(): X509Certificate {
return X509Utilities.loadCertificateFromPEMFile(loadResourceWithNullCheck(nutsDiscoveryProperties.cordaRootCertPath))
}
/**
* returns the Corda network-map certificate
*/
fun networkMapCertificate(): X509Certificate {
return X509Utilities.loadCertificateFromPEMFile(loadResourceWithNullCheck(nutsDiscoveryProperties.networkMapCertPath))
}
/**
* returns the Corda intermediate certificate
*/
fun intermediateCertificate(): X509Certificate {
return X509Utilities.loadCertificateFromPEMFile(loadResourceWithNullCheck(nutsDiscoveryProperties.intermediateCertPath))
}
/**
* Reads the key from disk and returns a PrivateKey instance
*/
fun networkMapKey(): PrivateKey {
val reader = PemReader(Files.newBufferedReader(loadResourceWithNullCheck(nutsDiscoveryProperties.networkMapKeyPath)) as Reader?)
val key = reader.readPemObject()
reader.close()
val kf = KeyFactory.getInstance("RSA") // or "EC" or whatever
return kf.generatePrivate(PKCS8EncodedKeySpec(key.content))
}
fun intermediateKeyPair(): KeyPair {
val keyReader = PemReader(Files.newBufferedReader(loadResourceWithNullCheck(nutsDiscoveryProperties.intermediateKeyPath)))
val key = keyReader.readPemObject()
keyReader.close()
val kf = KeyFactory.getInstance("RSA") // or "EC" or whatever
val priKey = kf.generatePrivate(PKCS8EncodedKeySpec(key.content))
return KeyPair(intermediateCertificate().publicKey, priKey)
}
/**
* Sign the network-map with the network-map key
*/
fun signNetworkMap(networkMap: NetworkMap): SignedNetworkMap {
return networkMap.signWithCert(networkMapKey(), networkMapCertificate())
}
/**
* Sign the network-map parameters with the network-map key
*/
fun signNetworkParams(networkParams: NetworkParameters): SignedNetworkParameters {
return networkParams.signWithCert(networkMapKey(), networkMapCertificate())
}
/**
* validate current setup, are all keys and certificates available?
*/
fun validate(): List<String> {
val configProblemSet = mutableMapOf(
Pair(::cordaRootCertificate, "root certificate"),
Pair(::intermediateCertificate, "intermediate certificate"),
Pair(::networkMapCertificate, "network map certificate"),
Pair(::intermediateCertificate, "intermediate key"),
Pair(::networkMapKey, "network map key")
)
val configProblems = mutableListOf<String>()
configProblemSet.forEach { (f, m) ->
try {
f.invoke()
} catch (e: Exception) {
configProblems.add("Failed to load $m, cause: ${e.message}")
}
}
return configProblems
}
}