Commit 79b8025c authored by Redford StopCovid's avatar Redford StopCovid

Merge branch 'release/v1.3.0' into 'develop'

Merge Back Release/v1.3.0

See merge request !75
parents 797b1abd 9a3cbd2e
......@@ -19,7 +19,7 @@
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.3.0-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
<name>robert-server</name>
<packaging>pom</packaging>
<description>Projet principal</description>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.3.0-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</parent>
<artifactId>robert-crypto-grpc-server-messaging</artifactId>
......
......@@ -18,4 +18,5 @@ public interface ICryptoServerGrpcClient {
Optional<GetInfoFromHelloMessageResponse> getInfoFromHelloMessage(GetInfoFromHelloMessageRequest request);
Optional<DeleteIdResponse> deleteId(DeleteIdRequest request);
}
......@@ -27,6 +27,24 @@ service CryptoGrpcServiceImpl {
rpc getIdFromStatus(GetIdFromStatusRequest) returns (GetIdFromStatusResponse){}
rpc getInfoFromHelloMessage(GetInfoFromHelloMessageRequest) returns (GetInfoFromHelloMessageResponse){}
rpc deleteId(DeleteIdRequest) returns (DeleteIdResponse){}
rpc reloadHSM(ReloadHSMRequest) returns (ReloadHSMResponse){}
rpc getHSMCacheStatus(HSMCacheStatusRequest) returns (HSMCacheStatusResponse){}
}
message ReloadHSMRequest {
string pin = 1;
string configFileName = 2;
}
message ReloadHSMResponse {
bool success = 1;
}
message HSMCacheStatusRequest {
}
message HSMCacheStatusResponse {
repeated string aliases = 1;
}
message CreateRegistrationRequest {
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.3.0-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</parent>
......
......@@ -3,6 +3,7 @@ package fr.gouv.stopc.robert.crypto.grpc.server.storage.cryptographic.service;
import java.security.Key;
import java.security.KeyPair;
import java.security.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
......@@ -30,4 +31,16 @@ public interface ICryptographicStorageService {
Key getKeyForEncryptingClientKeys();
//Key getKeyForEncryptingServerKeys();
/**
* Reload the HSM to be able to access the keys added daily
* @return Whether HSM reload was successful
*/
boolean reloadHSM(String password, String configFile);
/**
* Gets the complete list of keys cached from the HSM
* @return
*/
List<String> getHSMCacheStatus();
}
......@@ -14,17 +14,15 @@ import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAmount;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import javax.crypto.spec.SecretKeySpec;
import fr.gouv.stopc.robert.server.common.utils.ByteUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import fr.gouv.stopc.robert.crypto.grpc.server.storage.cryptographic.service.ICryptographicStorageService;
......@@ -36,16 +34,11 @@ import sun.security.pkcs11.SunPKCS11;
@Service
public class CryptographicStorageServiceImpl implements ICryptographicStorageService {
private KeyStore keyStore;
private static final int SERVER_KEY_SIZE = 24;
// Because Java does not know Skinny64, specify AES instead
private static final String KEYSTORE_SKINNY64_ALGONAME = "AES";
private static final String KEYSTORE_KEK_ALGONAME = "AES";
private static final String KEYSTORE_KG_ALGONAME = "AES";
private static final String SERVER_KEY_CERTIFICATE_CN = "CN=stopcovid.gouv.fr";
private static final String ECDH_ALGORITHM = "EC";
//private static final String ALIAS_SERVER_ECDH_PUBLIC_KEY = "server-ecdh-key";
private static final String ALIAS_SERVER_ECDH_PRIVATE_KEY = "register-key"; // ECDH
......@@ -54,6 +47,8 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
private static final String ALIAS_FEDERATION_KEY = "federation-key"; // K_G
private static final String ALIAS_SERVER_KEY_PREFIX = "server-key-"; // K_S
private static final String KEYSTORE_TYPE = "PKCS11";
private KeyPair keyPair;
// Cache for K_S keys
......@@ -63,8 +58,7 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
private Map<String, Key> kekCache;
private Provider provider;
private PublicKey publicKey;
private KeyStore keyStore;
private Key federationKeyCached;
......@@ -75,33 +69,47 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
throw new IllegalArgumentException("The init argument cannot be empty");
}
boolean hsmLoadSuccessful = this.loadSecurityProvider(password, configFile);
if (!hsmLoadSuccessful) {
throw new RuntimeException("Could not add security provider");
}
serverKeyCache = new HashMap<>();
kekCache = new HashMap<>();
}
private boolean loadSecurityProvider(String password, String configFile) {
try {
this.provider = new SunPKCS11(configFile);
if (-1 == Security.addProvider(provider)) {
throw new RuntimeException("could not add security provider");
// Required for JDK 1.8
this.provider = new SunPKCS11(configFile);
if (-1 == Security.addProvider(this.provider)) {
return false;
}
// For JDK 1.9+, uncomment line below and delete code above
//this.provider = Security.getProvider("SunPKCS11").configure(configFile);
char[] keyStorePassword = password.toCharArray();
this.keyStore = KeyStore.getInstance("PKCS11", provider);
this.keyStore = KeyStore.getInstance(KEYSTORE_TYPE, this.provider);
this.keyStore.load(null, keyStorePassword);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | ProviderException e) {
log.error("An expected error occured when trying to initialize the keyStore {} due to {}", e.getClass(), e.getMessage());
throw new RuntimeException("could not add security provider");
}
serverKeyCache = new HashMap<>();
kekCache = new HashMap<>();
log.error("An expected error occurred when trying to initialize the keyStore {} due to {}", e.getClass(), e.getMessage());
return false;
}
return true;
}
@Override
public boolean contains(String alias) {
try {
return this.keyStore.containsAlias(alias);
} catch (KeyStoreException e) {
log.info("An expected error occured when trying to check containing the alias {} due to {}", alias, e.getMessage());
synchronized (protectHsmReload) {
try {
return this.keyStore.containsAlias(alias);
} catch (KeyStoreException e) {
log.info("An expected error occurred when trying to check if keystore contains the alias {} due to {}", alias, e.getMessage());
}
}
return false;
}
......@@ -116,15 +124,22 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
// Cache the keypair
if (this.keyPair != null) {
return Optional.of(this.keyPair);
}
try {
PrivateKey privateKey = (PrivateKey) this.keyStore.getKey(ALIAS_SERVER_ECDH_PRIVATE_KEY, null);
PublicKey publicKey = this.keyStore.getCertificate(ALIAS_SERVER_ECDH_PRIVATE_KEY).getPublicKey();
this.keyPair = new KeyPair(publicKey, privateKey);
return Optional.ofNullable(this.keyPair);
} catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) {
log.error("Unable to retrieve the server key pair due to {}", e.getMessage());
} else {
synchronized (protectHsmReload) {
if (this.keyPair == null) {
try {
PrivateKey privateKey = (PrivateKey) this.keyStore.getKey(ALIAS_SERVER_ECDH_PRIVATE_KEY, null);
PublicKey publicKey = this.keyStore.getCertificate(ALIAS_SERVER_ECDH_PRIVATE_KEY).getPublicKey();
this.keyPair = new KeyPair(publicKey, privateKey);
return Optional.ofNullable(this.keyPair);
} catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) {
log.error("Unable to retrieve the server key pair due to {}", e.getMessage());
}
} else {
return Optional.of(this.keyPair);
}
}
}
return Optional.empty();
......@@ -133,12 +148,14 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
@Override
public byte[] getEntry(String alias) {
try {
if (this.contains(alias)) {
return this.keyStore.getKey(alias, null).getEncoded();
synchronized (protectHsmReload) {
try {
if (this.contains(alias)) {
return this.keyStore.getKey(alias, null).getEncoded();
}
} catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) {
log.error("An expected error occurred when trying to get the entry {} due to {}", alias, e.getMessage());
}
} catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) {
log.error("An expected error occured when trying to get the entry {} due to {}", alias, e.getMessage());
}
return null;
}
......@@ -159,13 +176,21 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
if (this.kekCache.containsKey(alias)) {
return this.kekCache.get(alias);
}
try {
Key key = this.keyStore.getKey(alias, null);
this.kekCache.put(alias, key);
return key;
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IllegalStateException e) {
log.error(errorMessage);
} else {
synchronized (protectHsmReload) {
if (!this.kekCache.containsKey(alias)) {
try {
Key key = this.keyStore.getKey(alias, null);
this.kekCache.put(alias, key);
return key;
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IllegalStateException e) {
log.error(errorMessage);
}
} else {
return this.kekCache.get(alias);
}
}
}
return null;
......@@ -194,17 +219,25 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
String alias = String.format("%s%s", ALIAS_SERVER_KEY_PREFIX, dateFromEpoch.format(dateFormatter));
if (this.serverKeyCache.containsKey(alias)) {
return this.serverKeyCache.get(alias);
serverKey = this.serverKeyCache.get(alias);
}
if (!this.keyStore.containsAlias(alias)) {
log.error("Key store does not contain key for alias {}", alias);
} else {
Key key = this.keyStore.getKey(alias, null);
serverKey = key.getEncoded();
this.serverKeyCache.put(alias, serverKey);
else {
synchronized (protectHsmReload) {
if (!this.serverKeyCache.containsKey(alias)) {
if (!this.keyStore.containsAlias(alias)) {
log.error("Key store does not contain key for alias {}", alias);
} else {
Key key = this.keyStore.getKey(alias, null);
serverKey = key.getEncoded();
this.serverKeyCache.put(alias, serverKey);
}
} else {
serverKey = this.serverKeyCache.get(alias);
}
}
}
} catch (Exception e) {
log.error("An expected error occured when trying to store the alias {} due to {}", dateFromEpoch, e.getMessage());
log.error("An expected error occurred when trying to get the alias {} due to {}", dateFromEpoch, e.getMessage());
}
return serverKey;
......@@ -231,13 +264,17 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
public Key getFederationKey() {
try {
if (this.federationKeyCached == null) {
if (this.keyStore.containsAlias(ALIAS_FEDERATION_KEY)) {
log.info("Fetching and caching federation key from keystore");
Key federationKeyFromHSM = this.keyStore.getKey(ALIAS_FEDERATION_KEY, null);
// TODO: review this and create issue tracking this behaviour
// Copy key content in new key to prevent any delegation to HSM and perform encryption in Java
this.federationKeyCached = new SecretKeySpec(federationKeyFromHSM.getEncoded(), KEYSTORE_KG_ALGONAME);
synchronized (protectHsmReload) {
if (this.federationKeyCached == null) {
if (this.keyStore.containsAlias(ALIAS_FEDERATION_KEY)) {
log.info("Fetching and caching federation key from keystore");
Key federationKeyFromHSM = this.keyStore.getKey(ALIAS_FEDERATION_KEY, null);
// TODO: review this and create issue tracking this behaviour
// Copy key content in new key to prevent any delegation to HSM and perform encryption in Java
this.federationKeyCached = new SecretKeySpec(federationKeyFromHSM.getEncoded(), KEYSTORE_KG_ALGONAME);
}
}
}
}
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
......@@ -245,4 +282,97 @@ public class CryptographicStorageServiceImpl implements ICryptographicStorageSer
}
return this.federationKeyCached;
}
private final Object protectHsmReload = new Object();
@Override
public boolean reloadHSM(String pin, String configName) {
log.info("HSM reload requested");
synchronized (protectHsmReload) {
log.info("Removing security provider");
Security.removeProvider(provider.getName());
// Flush keys
this.serverKeyCache.clear();
this.keyPair = null;
this.kekCache.clear();
log.info("Flushed cached keys");
boolean reloadResult = this.loadSecurityProvider(pin, configName);
if (!reloadResult) {
log.error("Could not reload Security Provider for HSM");
} else {
// Pre-warm cache
this.prewarmCache();
log.info("HSM reload successful");
}
return reloadResult;
}
}
private void prewarmCache() {
log.info("Pre-warming cache");
log.info("Caching federation key");
this.getFederationKey();
log.info("Caching key encryption key");
this.getKeyForEncryptingClientKeys();
log.info("Caching server key pair");
this.getServerKeyPair();
// Cache current and future keys
LocalDate dateForCachedServerKey = LocalDate.now(ZoneId.of("UTC"));
byte[] res;
do {
log.info("Caching future server key for {}", dateForCachedServerKey);
res = this.getServerKey(dateForCachedServerKey);
dateForCachedServerKey = dateForCachedServerKey.plusDays(1);
} while (res != null);
// Cache previous keys
dateForCachedServerKey = LocalDate.now(ZoneId.of("UTC")).minusDays(1);
do {
log.info("Caching past server key for {}", dateForCachedServerKey);
res = this.getServerKey(dateForCachedServerKey);
dateForCachedServerKey = dateForCachedServerKey.minusDays(1);
} while (res != null);
}
@Override
public List<String> getHSMCacheStatus() {
ArrayList<String> aliases = new ArrayList<>();
if (!Objects.isNull(this.keyPair)) {
if (!Objects.isNull(this.keyPair.getPublic())) {
aliases.add(String.format("Server Public ECDH Key '%s'", ALIAS_SERVER_ECDH_PRIVATE_KEY));
}
if (!Objects.isNull(this.keyPair.getPrivate())) {
aliases.add(String.format("Server Private ECDH Key '%s'", ALIAS_SERVER_ECDH_PRIVATE_KEY));
}
}
if (!CollectionUtils.isEmpty(this.kekCache)) {
aliases.addAll(this.kekCache.keySet().stream().map(elt -> String.format("Key Encryption Key '%s'", elt)).collect(Collectors.toList()));
}
if (!Objects.isNull(this.federationKeyCached)) {
aliases.add(String.format("Federation Key '%s'", ALIAS_FEDERATION_KEY));
}
if (!CollectionUtils.isEmpty(this.serverKeyCache)) {
aliases.addAll(this.serverKeyCache.keySet().stream().map(elt -> String.format("Server Key '%s'", elt)).collect(Collectors.toList()));
}
if (aliases.size() == 0) {
log.warn("HSM Cache Status yielded 0 keys");
} else {
log.info("HSM Cache Status yielded {} keys ", aliases.size());
}
aliases.sort(Comparator.naturalOrder());
return aliases;
}
}
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.3.0-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</parent>
<artifactId>robert-crypto-grpc-server</artifactId>
......
......@@ -2,12 +2,12 @@ package fr.gouv.stopc.robert.crypto.grpc.server.service;
public interface ICryptoServerConfigurationService {
/**
* TpStart in NTP seconds
*
* @return the time the ROBERT service was started (permanent, never changes,
* not tied to an instance)
*/
long getServiceTimeStart();
/**
* TpStart in NTP seconds
*
* @return the time the ROBERT service was started (permanent, never changes,
* not tied to an instance)
*/
long getServiceTimeStart();
}
......@@ -4,7 +4,12 @@ import java.security.Key;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
......@@ -27,6 +32,10 @@ import fr.gouv.stopc.robert.crypto.grpc.server.messaging.GetIdFromStatusRequest;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.GetIdFromStatusResponse;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.GetInfoFromHelloMessageRequest;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.GetInfoFromHelloMessageResponse;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.HSMCacheStatusRequest;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.HSMCacheStatusResponse;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.ReloadHSMRequest;
import fr.gouv.stopc.robert.crypto.grpc.server.messaging.ReloadHSMResponse;
import fr.gouv.stopc.robert.crypto.grpc.server.service.ICryptoServerConfigurationService;
import fr.gouv.stopc.robert.crypto.grpc.server.service.IECDHKeyService;
import fr.gouv.stopc.robert.crypto.grpc.server.storage.cryptographic.service.ICryptographicStorageService;
......@@ -79,6 +88,30 @@ public class CryptoGrpcServiceBaseImpl extends CryptoGrpcServiceImplImplBase {
this.propertyLoader = propertyLoader;
}
@Override
public void reloadHSM(ReloadHSMRequest request, StreamObserver<ReloadHSMResponse> responseObserver) {
boolean success = this.cryptographicStorageService.reloadHSM(
request.getPin(),
request.getConfigFileName());
responseObserver.onNext(ReloadHSMResponse.newBuilder()
.setSuccess(success)