org.forgerock.openidm.security.impl.SecurityResourceProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.security.impl.SecurityResourceProvider.java

Source

/*
 * 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());
        }
    }
}