/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2013-2015 ForgeRock AS. All Rights Reserved
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openidm.security.impl;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.cluster.ClusterUtils;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.openidm.crypto.factory.CryptoServiceFactory;
import org.forgerock.openidm.repo.RepositoryService;
import org.forgerock.openidm.security.KeyStoreHandler;
import org.forgerock.openidm.security.KeyStoreManager;
import org.forgerock.openidm.util.DateUtil;
import org.forgerock.util.encode.Base64;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class containing common members and methods of a Security ResourceProvider implementation.
*/
public class SecurityResourceProvider {
private final static Logger logger = LoggerFactory.getLogger(SecurityResourceProvider.class);
public static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
public static final String ACTION_GENERATE_CERT = "generateCert";
public static final String ACTION_GENERATE_CSR = "generateCSR";
public static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA512WithRSAEncryption";
public static final String DEFAULT_ALGORITHM = "RSA";
public static final String DEFAULT_CERTIFICATE_TYPE = "X509";
public static final int DEFAULT_KEY_SIZE = 2048;
public static final String KEYS_CONTAINER = "security/keys";
/**
* The Keystore handler which handles access to actual Keystore instance
*/
protected KeyStoreHandler store = null;
/**
* The KeyStoreManager used for reloading the stores.
*/
protected KeyStoreManager manager = null;
/**
* The RepositoryService
*/
protected RepositoryService repoService;
/**
* The resource name, "truststore" or "keystore".
*/
protected String resourceName = null;
/**
* The instance type (standalone, clustered-first, clustered-additional)
*/
private String instanceType;
private String cryptoAlias;
private String cryptoCipher;
public SecurityResourceProvider(String resourceName, KeyStoreHandler store, KeyStoreManager manager,
RepositoryService repoService) {
this.store = store;
this.resourceName = resourceName;
this.manager = manager;
this.repoService = repoService;
this.cryptoAlias = IdentityServer.getInstance().getProperty("openidm.config.crypto.alias");
this.cryptoCipher = ServerConstants.SECURITY_CRYPTOGRAPHY_DEFAULT_CIPHER;
this.instanceType = IdentityServer.getInstance().getProperty(
"openidm.instance.type", ClusterUtils.TYPE_STANDALONE);
}
/**
* Returns a PEM String representation of a object.
*
* @param object the object
* @return the PEM String representation
* @throws Exception
*/
protected String toPem(Object object) throws Exception {
StringWriter sw = new StringWriter();
PEMWriter pw = new PEMWriter(sw);
pw.writeObject(object);
pw.flush();
return sw.toString();
}
/**
* Returns an object from a PEM String representation
*
* @param pem the PEM String representation
* @return the object
* @throws Exception
*/
protected <T> T fromPem(String pem) throws Exception {
StringReader sr = new StringReader(pem);
PEMReader pw = new PEMReader(sr);
Object object = pw.readObject();
return (T)object;
}
/**
* Reads a certificate from a supplied string representation, and a supplied type.
*
* @param certString A String representation of a certificate
* @param type The type of certificate ("X509").
* @return The certificate
* @throws Exception
*/
protected Certificate readCertificate(String certString, String type) throws Exception {
StringReader sr = new StringReader(certString);
PEMReader pw = new PEMReader(sr);
Object object = pw.readObject();
if (object instanceof X509Certificate) {
return (X509Certificate)object;
} else {
throw ResourceException.getException(ResourceException.BAD_REQUEST, "Unsupported certificate format");
}
}
/**
* Reads a certificate chain from a supplied string array representation, and a supplied type.
*
* @param certStringChain an array of strings representing a certificate chain
* @param type the type of certificates ("X509")
* @return the certificate chain
* @throws Exception
*/
protected Certificate[] readCertificateChain(List<String> certStringChain, String type) throws Exception {
Certificate [] certChain = new Certificate[certStringChain.size()];
for (int i=0; i<certChain.length; i++) {
certChain[i] = readCertificate(certStringChain.get(i), type);
}
return certChain;
}
/**
* Returns a JsonValue map representing a certificate
*
* @param alias the certificate alias
* @param cert The certificate
* @return a JsonValue map representing the certificate
* @throws Exception
*/
protected JsonValue returnCertificate(String alias, Certificate cert) throws Exception {
JsonValue content = new JsonValue(new LinkedHashMap<String, Object>());
content.put(ResourceResponse.FIELD_CONTENT_ID, alias);
content.put("type", cert.getType());
content.put("cert", getCertString(cert));
content.put("publicKey", getKeyMap(cert.getPublicKey()));
if (cert instanceof X509Certificate) {
Map<String, Object> issuer = new HashMap<>();
X500Name name = X500Name.getInstance(PrincipalUtil.getIssuerX509Principal((X509Certificate)cert));
addAttributeToIssuer(issuer, name, "C", BCStyle.C);
addAttributeToIssuer(issuer, name, "ST", BCStyle.ST);
addAttributeToIssuer(issuer, name, "L", BCStyle.L);
addAttributeToIssuer(issuer, name, "OU", BCStyle.OU);
addAttributeToIssuer(issuer, name, "O", BCStyle.O);
addAttributeToIssuer(issuer, name, "CN", BCStyle.CN);
content.put("issuer", issuer);
content.put("notBefore", ((X509Certificate)cert).getNotBefore());
content.put("notAfter", ((X509Certificate)cert).getNotAfter());
}
return content;
}
/**
* Returns a JsonValue map representing a CSR
*
* @param alias the certificate alias
* @param csr The CSR
* @return a JsonValue map representing the CSR
* @throws Exception
*/
protected JsonValue returnCertificateRequest(String alias, PKCS10CertificationRequest csr) throws Exception {
JsonValue content = new JsonValue(new LinkedHashMap<String, Object>());
content.put(ResourceResponse.FIELD_CONTENT_ID, alias);
content.put("csr", getCertString(csr));
content.put("publicKey", getKeyMap(csr.getPublicKey()));
return content;
}
/**
* Returns a JsonValue map representing a CSR
*
* @param alias the certificate alias
* @param key The key
* @return a JsonValue map representing the CSR
* @throws Exception
*/
protected JsonValue returnKey(String alias, Key key) throws Exception {
JsonValue content = new JsonValue(new LinkedHashMap<String, Object>());
content.put(ResourceResponse.FIELD_CONTENT_ID, alias);
if (key instanceof PrivateKey) {
content.put("privateKey", getKeyMap(key));
} else if (key instanceof SecretKey) {
content.put("secret", getSecretKeyMap(key));
}
return content;
}
/**
* Returns a JsonValue map representing key
*
* @param key The key
* @return a JsonValue map representing the key
* @throws Exception
*/
protected Map<String, Object> getKeyMap(Key key) throws Exception {
Map<String, Object> keyMap = new HashMap<>();
keyMap.put("algorithm", key.getAlgorithm());
keyMap.put("format", key.getFormat());
keyMap.put("encoded", toPem(key));
return keyMap;
}
/**
* Returns a JsonValue map representing key
*
* @param key The key
* @return a JsonValue map representing the key
* @throws Exception
*/
protected Map<String, Object> getSecretKeyMap(Key key) throws Exception {
Map<String, Object> keyMap = new HashMap<>();
keyMap.put("algorithm", key.getAlgorithm());
keyMap.put("format", key.getFormat());
keyMap.put("encoded", Base64.encode(key.getEncoded()));
return keyMap;
}
/**
* Returns a PEM formatted string representation of an object
*
* @param object the object to write
* @return a PEM formatted string representation of the object
* @throws Exception
*/
protected String getCertString(Object object) throws Exception {
PEMWriter pemWriter = null;
StringWriter sw = null;
try {
sw = new StringWriter();
pemWriter = new PEMWriter(sw);
pemWriter.writeObject(object);
pemWriter.flush();
} finally {
pemWriter.close();
}
return sw.getBuffer().toString();
}
/**
* Generates a self signed certificate using the given properties.
*
* @param commonName the common name to use for the new certificate
* @param algorithm the algorithm to use
* @param keySize the keysize to use
* @param signatureAlgorithm the signature algorithm to use
* @param validFrom when the certificate is valid from
* @param validTo when the certificate is valid until
* @return The generated certificate
* @throws Exception
*/
protected Pair<X509Certificate, PrivateKey> generateCertificate(String commonName,
String algorithm, int keySize, String signatureAlgorithm, String validFrom,
String validTo) throws Exception {
return generateCertificate(commonName, "None", "None", "None", "None", "None",
algorithm, keySize, signatureAlgorithm, validFrom, validTo);
}
/**
* Generates a self signed certificate using the given properties.
*
* @param commonName the subject's common name
* @param organization the subject's organization name
* @param organizationUnit the subject's organization unit name
* @param stateOrProvince the subject's state or province
* @param country the subject's country code
* @param locality the subject's locality
* @param algorithm the algorithm to use
* @param keySize the keysize to use
* @param signatureAlgorithm the signature algorithm to use
* @param validFrom when the certificate is valid from
* @param validTo when the certificate is valid until
* @return The generated certificate
* @throws Exception
*/
protected Pair<X509Certificate, PrivateKey> generateCertificate(String commonName,
String organization, String organizationUnit, String stateOrProvince,
String country, String locality, String algorithm, int keySize,
String signatureAlgorithm, String validFrom, String validTo) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm); // "RSA","BC"
keyPairGenerator.initialize(keySize);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Generate self-signed certificate
X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
builder.addRDN(BCStyle.C, country);
builder.addRDN(BCStyle.ST, stateOrProvince);
builder.addRDN(BCStyle.L, locality);
builder.addRDN(BCStyle.OU, organizationUnit);
builder.addRDN(BCStyle.O, organization);
builder.addRDN(BCStyle.CN, commonName);
Date notBefore = null;
Date notAfter = null;
if (validFrom == null) {
notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30);
} else {
DateTime notBeforeDateTime = DateUtil.getDateUtil().parseIfDate(validFrom);
if (notBeforeDateTime == null) {
throw new InternalServerErrorException("Invalid date format for 'validFrom' property");
} else {
notBefore = notBeforeDateTime.toDate();
}
}
if (validTo == null) {
Calendar date = Calendar.getInstance();
date.setTime(new Date());
date.add(Calendar.YEAR, 10);
notAfter = date.getTime();
} else {
DateTime notAfterDateTime = DateUtil.getDateUtil().parseIfDate(validTo);
if (notAfterDateTime == null) {
throw new InternalServerErrorException("Invalid date format for 'validTo' property");
} else {
notAfter = notAfterDateTime.toDate();
}
}
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
X509v3CertificateBuilder v3CertGen = new JcaX509v3CertificateBuilder(builder.build(), serial,
notBefore, notAfter, builder.build(), keyPair.getPublic());
ContentSigner sigGen =
new JcaContentSignerBuilder(signatureAlgorithm).setProvider(BC).build(keyPair.getPrivate());
X509Certificate cert =
new JcaX509CertificateConverter().setProvider(BC).getCertificate(v3CertGen.build(sigGen));
cert.checkValidity(new Date());
cert.verify(cert.getPublicKey());
return Pair.of(cert, keyPair.getPrivate());
}
/**
* Generates a CSR request.
*
* @param alias
* @param algorithm
* @param signatureAlgorithm
* @param keySize
* @param params
* @return
* @throws Exception
*/
protected Pair<PKCS10CertificationRequest, PrivateKey> generateCSR(String alias, String algorithm,
String signatureAlgorithm, int keySize, JsonValue params) throws Exception {
// Construct the distinguished name
StringBuilder sb = new StringBuilder();
sb.append("CN=").append(params.get("CN").required().asString().replaceAll(",", "\\\\,"));
sb.append(", OU=").append(params.get("OU").defaultTo("None").asString().replaceAll(",", "\\\\,"));
sb.append(", O=").append(params.get("O").defaultTo("None").asString().replaceAll(",", "\\\\,"));
sb.append(", L=").append(params.get("L").defaultTo("None").asString().replaceAll(",", "\\\\,"));
sb.append(", ST=").append(params.get("ST").defaultTo("None").asString().replaceAll(",", "\\\\,"));
sb.append(", C=").append(params.get("C").defaultTo("None").asString().replaceAll(",", "\\\\,"));
// Create the principle subject name
X509Principal subjectName = new X509Principal(sb.toString());
//store.getStore().
// Generate the key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
keyPairGenerator.initialize(keySize);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// Generate the certificate request
PKCS10CertificationRequest cr = new PKCS10CertificationRequest(signatureAlgorithm, subjectName, publicKey,
null, privateKey);
// Store the private key to use when the signed cert is return and updated
logger.debug("Storing private key with alias {}", alias);
storeKeyPair(alias, keyPair);
return Pair.of(cr, privateKey);
}
/**
* Stores a KeyPair (associated with a CSR request on the specified alias) in the repository.
*
* @param alias the alias from the CSR
* @param keyPair the KeyPair object
* @throws ResourceException
*/
protected void storeKeyPair(String alias, KeyPair keyPair) throws ResourceException {
try {
JsonValue keyPairValue = new JsonValue(new HashMap<String, Object>());
keyPairValue.put("value" , toPem(keyPair));
JsonValue encrypted = getCryptoService().encrypt(keyPairValue, cryptoCipher, cryptoAlias);
JsonValue keyMap = new JsonValue(new HashMap<String, Object>());
keyMap.put("keyPair", encrypted.getObject());
storeInRepo(KEYS_CONTAINER, alias, keyMap);
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
}
/**
* Reads an object from the repository
* @param id the object's id
* @return the object
* @throws ResourceException
*/
protected JsonValue readFromRepo(String id) throws ResourceException {
JsonValue keyMap = new JsonValue(repoService.read(Requests.newReadRequest(id)).getContent());
return keyMap;
}
/**
* Stores an object in the repository
* @param id the object's id
* @param value the value of the object to store
* @throws ResourceException
*/
protected void storeInRepo(String container, String id, JsonValue value) throws ResourceException {
ResourceResponse oldResource;
try {
oldResource = repoService.read(Requests.newReadRequest(container, id));
} catch (NotFoundException e) {
logger.debug("creating object " + id);
repoService.create(Requests.newCreateRequest(container, id, value));
return;
}
UpdateRequest updateRequest = Requests.newUpdateRequest(container, id, value);
updateRequest.setRevision(oldResource.getRevision());
repoService.update(updateRequest);
}
/**
* Returns a stored KeyPair (associated with a CSR request on the specified alias) from the repository.
*
* @param alias the alias from the CSR
* @return the KeyPair
* @throws ResourceException
*/
protected KeyPair getKeyPair(String alias) throws ResourceException {
String id = KEYS_CONTAINER + "/" + alias;
ResourceResponse keyResource = repoService.read(Requests.newReadRequest(id));
if (keyResource.getContent().isNull()) {
throw new NotFoundException("Cannot find stored key for alias " + alias);
}
try {
JsonValue encrypted = keyResource.getContent().get("keyPair");
JsonValue keyPairValue = getCryptoService().decrypt(encrypted);
return fromPem(keyPairValue.get("value").asString());
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
}
/**
* Verifies that the supplied private key and signed certificate match by signing/verifying some test data.
*
* @param privateKey A private key
* @param cert the certificate
* @throws ResourceException if the verification fails, or an error is encountered.
*/
protected void verify(PrivateKey privateKey, Certificate cert) throws ResourceException {
PublicKey publicKey = cert.getPublicKey();
byte[] data = { 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 };
boolean verified;
try {
Signature signer = Signature.getInstance(privateKey.getAlgorithm());
signer.initSign(privateKey);
signer.update(data);
byte[] signed = signer.sign();
Signature verifier = Signature.getInstance(publicKey.getAlgorithm());
verifier.initVerify(publicKey);
verifier.update(data);
verified = verifier.verify(signed);
} catch (Exception e) {
throw new InternalServerErrorException("Error verifying private key and signed certificate", e);
}
if (!verified) {
throw new BadRequestException("Private key does not match signed certificate");
}
}
/**
* Saves the local store only if in a clustered environment.
*
* @throws ResourceException
*/
protected void saveStore() throws ResourceException {
if (!instanceType.equals(ClusterUtils.TYPE_STANDALONE)) {
saveStoreToRepo();
}
}
/**
* Loads the store from the repository and stores it locally
*
* @throws ResourceException
*/
public void loadStoreFromRepo() throws ResourceException {
JsonValue keystoreValue = readFromRepo("security/" + resourceName);
String keystoreString = keystoreValue.get("storeString").asString();
byte [] keystoreBytes = Base64.decode(keystoreString.getBytes());
ByteArrayInputStream bais = new ByteArrayInputStream(keystoreBytes);
try {
KeyStore keystore = null;
try {
keystore = KeyStore.getInstance(store.getType());
keystore.load(bais, store.getPassword().toCharArray());
} finally {
bais.close();
}
store.setStore(keystore);
} catch (Exception e) {
// Note this may catch NPE from Base64.decode returning null if keyStoreString
// is null or not a base64-encoded string
throw new InternalServerErrorException("Error creating keystore from store bytes", e);
}
}
/**
* Saves the local store to the repository
*
* @throws ResourceException
*/
public void saveStoreToRepo() throws ResourceException {
byte [] keystoreBytes = null;
FileInputStream fin = null;
File file = new File(store.getLocation());
try {
try {
fin = new FileInputStream(file);
keystoreBytes = new byte[(int) file.length()];
fin.read(keystoreBytes);
} finally {
fin.close();
}
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
String keystoreString = new String(Base64.encode(keystoreBytes));
JsonValue value = new JsonValue(new HashMap<String, Object>());
value.add("storeString", keystoreString);
storeInRepo("security", resourceName, value);
}
/**
* Returns and instance of the CryptoService.
*
* @return CryptoService instance.
*/
private CryptoService getCryptoService() {
return CryptoServiceFactory.getInstance();
}
/**
* Adds an attribute to an issuer map object if it exists in the supplied X500Name object.
*
* @param issuer The issuer to add to
* @param name The X500Name object
* @param attribute the name of the attribute
* @param oid the ASN1ObjectIdentifier corresponding to the attribute
* @throws Exception
*/
private void addAttributeToIssuer(Map<String, Object> issuer, X500Name name, String attribute,
ASN1ObjectIdentifier oid) throws Exception {
RDN [] rdns = name.getRDNs(oid);
if (rdns != null && rdns.length > 0) {
issuer.put(attribute, rdns[0].getFirst().getValue().toString());
}
}
}