JamsCA.java

/*
 * Copyright (C) 2020-2024 by Savoir-faire Linux
 *
 * 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 net.jami.jams.ca;

import com.google.gson.Gson;

import lombok.extern.slf4j.Slf4j;

import net.jami.jams.ca.workers.crl.CRLWorker;
import net.jami.jams.ca.workers.crl.RevocationCallback;
import net.jami.jams.ca.workers.csr.CertificateWorker;
import net.jami.jams.ca.workers.ocsp.OCSPWorker;
import net.jami.jams.common.cryptoengineapi.CertificateAuthority;
import net.jami.jams.common.cryptoengineapi.CertificateAuthorityConfig;
import net.jami.jams.common.objects.devices.Device;
import net.jami.jams.common.objects.requests.RevocationRequest;
import net.jami.jams.common.objects.system.SystemAccount;
import net.jami.jams.common.objects.user.User;
import net.jami.jams.common.serialization.adapters.GsonFactory;

import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
public class JamsCA implements CertificateAuthority, RevocationCallback {

    // These are the workers which are responsible for CRL/OCSP, they have an odd relationship.
    private static CRLWorker crlWorker;
    private static OCSPWorker ocspWorker;
    public static volatile String serverDomain;
    // The default value is SHA512WITHRSA, this can be changed in the config file.
    public static volatile String signingAlgorithm = "SHA512WITHRSA";

    // Various times
    public static long crlLifetime = 360_000_000;
    public static long userLifetime = 360_000_000;
    public static long deviceLifetime = 360_000_000;

    // CA certificate & OCSP Certificates, because they are often used.
    public static SystemAccount CA;
    public static SystemAccount OCSP;
    // Whether JAMS is behind a reverse proxy or not.
    public static boolean reverseProxy = false;
    // Flag to indicate completion of the revokeCertificate method
    private boolean revokeCompleted = false;
    private final Object lock = new Object();

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Override
    public void init(String settings, SystemAccount ca, SystemAccount ocsp) {
        Gson gson = GsonFactory.createGson();
        CertificateAuthorityConfig config =
                gson.fromJson(settings, CertificateAuthorityConfig.class);
        CA = ca;
        OCSP = ocsp;
        serverDomain = config.getServerDomain();
        signingAlgorithm = config.getSigningAlgorithm();

        crlLifetime = config.getCrlLifetime();
        userLifetime = config.getUserLifetime();
        deviceLifetime = config.getDeviceLifetime();
        reverseProxy = config.getReverseProxy();

        if (deviceLifetime > userLifetime) {
            log.warn(
                    "Device lifetime is greater than user lifetime, this is not recommended, please change this in the config file.");
        }

        X509Certificate cert = ca.getCertificate();
        long caLifetime = cert.getNotAfter().getTime() - cert.getNotBefore().getTime();

        if (userLifetime > caLifetime) {
            log.warn(
                    "User lifetime is greater than CA lifetime, this is not recommended, please change this in the config file.");
        }

        if (ca != null && ocsp != null) {
            crlWorker = new CRLWorker(CA.getPrivateKey(), CA.getCertificate());
            crlWorker.setRevocationCallback(this);
            try {
                ocspWorker = new OCSPWorker(OCSP.getPrivateKey(), OCSP.getCertificate(), crlWorker);
            } catch (Exception e) {
                log.error("Could not start OCSP request processor with error {}", e.getMessage());
            }
        }
    }

    @Override
    public User getSignedCertificate(User user) {
        return CertificateWorker.getSignedCertificate(user);
    }

    @Override
    public User getRefreshedCertificate(User user) {
        return CertificateWorker.getRefreshedCertificate(user);
    }

    @Override
    public Device getSignedCertificate(User user, Device device) {
        return CertificateWorker.getSignedCertificate(user, device);
    }

    @Override
    public SystemAccount getSignedCertificate(SystemAccount systemAccount) {
        return CertificateWorker.getSignedCertificate(systemAccount);
    }

    @Override
    public void revokeCertificate(RevocationRequest revocationRequest) {
        crlWorker.getInput().add(revocationRequest);
        synchronized (crlWorker.getInput()) {
            crlWorker.getInput().notify();
        }
    }

    @Override
    public void onRevocationCompleted() {
        revokeCompleted = true;
        // Notify waiting threads
        synchronized (lock) {
            lock.notifyAll();
        }
    }

    public synchronized void waitForRevokeCompletion() throws InterruptedException {
        synchronized (lock) {
            while (!revokeCompleted) {
                lock.wait(); // Wait until revokeCertificate completes and notifies
            }
            // Reset the flag after processing the completion notification
            revokeCompleted = false;
        }
    }

    @Override
    public AtomicReference<X509CRLHolder> getLatestCRL() {
        return crlWorker.getExistingCRL();
    }

    @Override
    public String getLatestCRLPEMEncoded() {
        try {
            return Base64.getEncoder().encodeToString(getLatestCRL().get().getEncoded());
        } catch (Exception e) {
            log.error("Could not return a valid CRL!");
            return null;
        }
    }

    public static OCSPResp getOCSPResponse(
            OCSPReq ocspRequest,
            X509Certificate certificate,
            PrivateKey privateKey,
            Boolean unknown)
            throws OCSPException {
        return ocspWorker.getOCSPResponse(ocspRequest, certificate, privateKey, unknown);
    }

    @Override
    public X509Certificate getCA() {
        return CA.getCertificate();
    }

    @Override
    public boolean shutdownThreads() {
        // Unsafe but acceptable.
        crlWorker.getStop().set(true);
        crlWorker.interrupt();
        Thread.State state = crlWorker.getState();
        while (!state.equals(Thread.State.TERMINATED)) {
            state = crlWorker.getState();
        }
        crlWorker = null;
        ocspWorker.stop();
        return true;
    }
}