Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update cryptography tools to support tssEncryptionKey loading and generation #16780

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions hedera-node/hedera-app/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import com.swirlds.config.api.ConfigurationExtension;

module com.hedera.node.app {
uses com.hedera.cryptography.pairings.api.PairingFriendlyCurve;

requires transitive com.hedera.node.app.hapi.utils;
requires transitive com.hedera.node.app.service.addressbook.impl;
requires transitive com.hedera.node.app.service.addressbook;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
import static com.swirlds.platform.crypto.CryptoConstants.PUBLIC_KEYS_FILE;
import static com.swirlds.platform.crypto.KeyCertPurpose.SIGNING;

import com.hedera.cryptography.bls.BlsKeyPair;
import com.hedera.cryptography.bls.GroupAssignment;
import com.hedera.cryptography.bls.SignatureSchema;
import com.hedera.cryptography.pairings.api.Curve;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.common.crypto.CryptographyException;
import com.swirlds.common.crypto.config.CryptoConfig;
Expand Down Expand Up @@ -93,6 +97,8 @@
* A collection of various static crypto methods
*/
public final class CryptoStatic {
public static final SignatureSchema SIGNATURE_SCHEMA =
SignatureSchema.create(Curve.ALT_BN128, GroupAssignment.SHORT_SIGNATURES);
private static final Logger logger = LogManager.getLogger(CryptoStatic.class);
private static final int SERIAL_NUMBER_BITS = 64;
private static final int MASTER_KEY_MULTIPLIER = 157;
Expand Down Expand Up @@ -626,6 +632,22 @@
return store;
}

/**
* Generate a {@link BlsKeyPair} using a {@link SignatureSchema} and a {@link SecureRandom} instance
* @param secureRandom the secure random number generator to use
* @return a new {@link BlsKeyPair}
derektriley marked this conversation as resolved.
Show resolved Hide resolved
* @throws NoSuchAlgorithmException the algorithm is not supported
*/
public static BlsKeyPair generateBlsKeyPair(@Nullable final SecureRandom secureRandom)
throws NoSuchAlgorithmException {
if (secureRandom == null) {
logger.debug("Generating a new BLS key pair using a default secure random number generator");
return BlsKeyPair.generate(SIGNATURE_SCHEMA);

Check warning on line 645 in platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java

View check run for this annotation

Codecov / codecov/patch

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/CryptoStatic.java#L644-L645

Added lines #L644 - L645 were not covered by tests
derektriley marked this conversation as resolved.
Show resolved Hide resolved
}
logger.debug("Generating a new BLS key pair using a custom secure random number generator");
return BlsKeyPair.generate(SIGNATURE_SCHEMA, secureRandom);
}

/**
* Check if a certificate is valid. A certificate is valid if it is not null, has a public key, and can be encoded.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
import static com.swirlds.platform.crypto.CryptoStatic.loadKeys;
import static com.swirlds.platform.state.address.AddressBookNetworkUtils.isLocal;

import com.hedera.cryptography.asciiarmored.AsciiArmoredFiles;
import com.hedera.cryptography.bls.BlsKeyPair;
import com.hedera.cryptography.bls.BlsPrivateKey;
import com.hedera.cryptography.bls.BlsPublicKey;
import com.swirlds.common.crypto.config.CryptoConfig;
import com.swirlds.common.platform.NodeId;
import com.swirlds.config.api.Configuration;
Expand Down Expand Up @@ -204,6 +208,16 @@
*/
private final Map<NodeId, PrivateKey> agrPrivateKeys;

/**
* The private tss encryption keys loaded from disk.
*/
private final Map<NodeId, BlsPrivateKey> tssPrivateKeys;

/**
* The public tss encryption keys.
*/
private final Map<NodeId, BlsPublicKey> tssPublicKeys;

/**
* The X.509 Certificates loaded from the key stores.
*/
Expand Down Expand Up @@ -244,6 +258,8 @@
this.agrPrivateKeys = HashMap.newHashMap(addressBook.getSize());
this.agrCertificates = HashMap.newHashMap(addressBook.getSize());
this.localNodes = HashSet.newHashSet(addressBook.getSize());
this.tssPrivateKeys = HashMap.newHashMap(addressBook.getSize());
this.tssPublicKeys = HashMap.newHashMap(addressBook.getSize());
}

/**
Expand Down Expand Up @@ -298,6 +314,11 @@
localNodes.add(nodeId);
sigPrivateKeys.compute(
nodeId, (k, v) -> resolveNodePrivateKey(nodeId, nodeAlias, KeyCertPurpose.SIGNING));

BlsPrivateKey tssPrivateKey = resolveTssPrivateKey(nodeId, nodeAlias);
if (tssPrivateKey != null) {
tssPrivateKeys.put(nodeId, tssPrivateKey);
}
}

sigCertificates.compute(
Expand All @@ -309,6 +330,29 @@
return this;
}

@Nullable
private BlsPrivateKey resolveTssPrivateKey(@NonNull final NodeId nodeId, @NonNull final String nodeAlias)
throws KeyLoadingException {
Objects.requireNonNull(nodeId, MSG_NODE_ID_NON_NULL);
Objects.requireNonNull(nodeAlias, MSG_NODE_ALIAS_NON_NULL);

Path keyLocation = keyStoreDirectory.resolve(String.format("t-private-%s.tss", nodeAlias));
derektriley marked this conversation as resolved.
Show resolved Hide resolved
if (Files.exists(keyLocation)) {
logger.trace(
STARTUP.getMarker(),
"Found tss encryption private key for node {} [ fileName = {} ]",
nodeId,
keyLocation.getFileName());
try {
return AsciiArmoredFiles.readPrivateKey(keyLocation);
} catch (final Exception e) {
logger.error(ERROR.getMarker(), "Failed to read TSS encryption private key from disk", e);
throw new KeyLoadingException("Failed to read TSS encryption private key from disk", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to fail catastrophically by throwing an exception? Or can we return null and let the private key be regenerated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we could return null and let the private key be regenerated.

However, this behavior should be clear in the node operator documentation. In the case of the private key becoming corrupted on-disk, it would be regenerated and a new public key submitted without giving the operator a chance to restore the private key from a backup.

}
}
return null;
}

/**
* Iterates over the local nodes and creates the agreement key and certificate for each. This method should be
* called after {@link #scan()} and before {@link #verify()}.
Expand Down Expand Up @@ -349,6 +393,28 @@
SecureRandom.getInstanceStrong());
agrCertificates.put(node, agrCert);
}

if (!tssPrivateKeys.containsKey(node) || tssPrivateKeys.get(node) == null) {
// Create a new public/private key pair for the TSS encryption key
BlsKeyPair tssKeyPair = CryptoStatic.generateBlsKeyPair(SecureRandom.getInstanceStrong());
tssPrivateKeys.put(node, tssKeyPair.privateKey());
tssPublicKeys.put(node, tssKeyPair.publicKey());
// Write the private key to disk
final String nodeAlias =
nameToAlias(addressBook.getAddress(node).getSelfName());
Path tssEncryptionPrivateKeyLocation =
keyStoreDirectory.resolve(String.format("t-private-%s.tss", nodeAlias));
try {
AsciiArmoredFiles.writeKey(tssEncryptionPrivateKeyLocation, tssKeyPair.privateKey());
} catch (IOException e) {
logger.error(ERROR.getMarker(), "Failed to write TSS encryption private key to disk", e);
throw new KeyGeneratingException("Failed to write TSS encryption private key to disk", e);

Check warning on line 411 in platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java

View check run for this annotation

Codecov / codecov/patch

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java#L409-L411

Added lines #L409 - L411 were not covered by tests
}
} else {
// Create the BlsPublicKey from the private TSS encryption key
BlsPrivateKey tssPrivateKey = tssPrivateKeys.get(node);
tssPublicKeys.put(node, tssPrivateKey.createPublicKey());
}
}
return this;
}
Expand Down Expand Up @@ -406,6 +472,16 @@
throw new KeyLoadingException("No certificate found for node %s [ alias = %s, purpose = %s ]"
.formatted(nodeId, nodeAlias, KeyCertPurpose.AGREEMENT));
}

if (!tssPrivateKeys.containsKey(nodeId)) {
throw new KeyLoadingException(
"No TSS private key found for node %s [ alias = %s ]".formatted(nodeId, nodeAlias));

Check warning on line 478 in platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java

View check run for this annotation

Codecov / codecov/patch

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java#L477-L478

Added lines #L477 - L478 were not covered by tests
}

if (!tssPublicKeys.containsKey(nodeId)) {
throw new KeyLoadingException(
"No TSS public key found for node %s [ alias = %s ]".formatted(nodeId, nodeAlias));

Check warning on line 483 in platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java

View check run for this annotation

Codecov / codecov/patch

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/EnhancedKeyStoreLoader.java#L482-L483

Added lines #L482 - L483 were not covered by tests
}
}

if (!sigCertificates.containsKey(nodeId)) {
Expand Down Expand Up @@ -481,7 +557,11 @@
final KeyPair sigKeyPair = new KeyPair(sigCert.getPublicKey(), sigPrivateKey);
final KeyPair agrKeyPair = new KeyPair(agrCert.getPublicKey(), agrPrivateKey);

final KeysAndCerts kc = new KeysAndCerts(sigKeyPair, agrKeyPair, sigCert, agrCert, publicStores);
final BlsPrivateKey tssPrivateKey = tssPrivateKeys.get(nodeId);
final BlsPublicKey tssPublicKey = tssPublicKeys.get(nodeId);

final KeysAndCerts kc = new KeysAndCerts(
sigKeyPair, agrKeyPair, sigCert, agrCert, publicStores, tssPrivateKey, tssPublicKey);

keysAndCerts.put(nodeId, kc);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package com.swirlds.platform.crypto;

import com.hedera.cryptography.bls.BlsKeyPair;
import com.hedera.cryptography.bls.BlsPrivateKey;
import com.hedera.cryptography.bls.BlsPublicKey;
import com.swirlds.common.crypto.internal.CryptoUtils;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.security.Key;
Expand Down Expand Up @@ -59,9 +62,12 @@
KeyPair agrKeyPair,
X509Certificate sigCert,
X509Certificate agrCert,
PublicStores publicStores) {
PublicStores publicStores,
BlsPrivateKey privateTssEncryptionKey,
BlsPublicKey publicTssEncryptionKey) {
private static final int SIG_SEED = 2;
private static final int AGR_SEED = 0;
private static final int TSS_ENCRYPTION_KEY_SEED = 3;

/**
* Creates an instance holding all the keys and certificates. It reads its own key pairs from privateKeyStore
Expand Down Expand Up @@ -111,7 +117,7 @@
publicStores.setCertificate(KeyCertPurpose.AGREEMENT, agreementCert, dnA);
}

return new KeysAndCerts(signingKeyPair, agreementKeyPair, signingCert, agreementCert, publicStores);
return new KeysAndCerts(signingKeyPair, agreementKeyPair, signingCert, agreementCert, publicStores, null, null);

Check warning on line 120 in platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java

View check run for this annotation

Codecov / codecov/patch

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/crypto/KeysAndCerts.java#L120

Added line #L120 was not covered by tests
}

private static KeyPair getKeyPair(final KeyStore privateKeyStore, final char[] password, final String storeName)
Expand Down Expand Up @@ -152,12 +158,14 @@

final SecureRandom sigDetRandom; // deterministic CSPRNG, used briefly then discarded
final SecureRandom agrDetRandom; // deterministic CSPRNG, used briefly then discarded
final SecureRandom tssEncryptionKeyRandom;

sigKeyGen = KeyPairGenerator.getInstance(CryptoConstants.SIG_TYPE1, CryptoConstants.SIG_PROVIDER);
agrKeyGen = KeyPairGenerator.getInstance(CryptoConstants.AGR_TYPE, CryptoConstants.AGR_PROVIDER);

sigDetRandom = CryptoUtils.getDetRandom(); // deterministic, not shared
agrDetRandom = CryptoUtils.getDetRandom(); // deterministic, not shared
tssEncryptionKeyRandom = CryptoUtils.getDetRandom(); // deterministic, not shared

sigDetRandom.setSeed(masterKey);
sigDetRandom.setSeed(swirldId);
Expand All @@ -171,6 +179,11 @@
agrDetRandom.setSeed(AGR_SEED);
agrKeyGen.initialize(CryptoConstants.AGR_KEY_SIZE_BITS, agrDetRandom);

tssEncryptionKeyRandom.setSeed(masterKey);
derektriley marked this conversation as resolved.
Show resolved Hide resolved
tssEncryptionKeyRandom.setSeed(swirldId);
tssEncryptionKeyRandom.setSeed(memberId);
tssEncryptionKeyRandom.setSeed(TSS_ENCRYPTION_KEY_SEED);

final KeyPair sigKeyPair = sigKeyGen.generateKeyPair();
final KeyPair agrKeyPair = agrKeyGen.generateKeyPair();

Expand All @@ -188,7 +201,16 @@
publicStores.setCertificate(KeyCertPurpose.SIGNING, sigCert, name);
publicStores.setCertificate(KeyCertPurpose.AGREEMENT, agrCert, name);

return new KeysAndCerts(sigKeyPair, agrKeyPair, sigCert, agrCert, publicStores);
final BlsKeyPair blsKeyPair = CryptoStatic.generateBlsKeyPair(tssEncryptionKeyRandom);

return new KeysAndCerts(
sigKeyPair,
agrKeyPair,
sigCert,
agrCert,
publicStores,
blsKeyPair.privateKey(),
blsKeyPair.publicKey());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
requires transitive com.swirlds.state.impl;
requires transitive com.fasterxml.jackson.annotation;
requires transitive com.fasterxml.jackson.databind;
requires transitive com.hedera.cryptography.bls;
requires transitive com.hedera.pbj.runtime;
requires transitive info.picocli;
requires transitive org.apache.logging.log4j;
Expand All @@ -150,6 +151,7 @@
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.dataformat.yaml;
requires com.github.spotbugs.annotations;
requires com.hedera.cryptography.pairings.api;
requires java.desktop;
requires java.management;
requires java.scripting;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,18 @@ void validateTestDataDirectory() {
assertThat(testDataDirectory.resolve("enhanced-valid-no-agreement-key"))
.exists()
.isNotEmptyDirectory();
assertThat(testDataDirectory.resolve("enhanced-valid-no-tss-key"))
.exists()
.isNotEmptyDirectory();
assertThat(testDataDirectory.resolve("enhanced-invalid-case-1"))
.exists()
.isNotEmptyDirectory();
assertThat(testDataDirectory.resolve("enhanced-invalid-case-2"))
.exists()
.isNotEmptyDirectory();
assertThat(testDataDirectory.resolve("enhanced-invalid-case-3"))
.exists()
.isNotEmptyDirectory();
assertThat(testDataDirectory.resolve("legacy-valid").resolve("public.pfx"))
.exists()
.isNotEmptyFile();
Expand All @@ -105,7 +111,14 @@ void validateTestDataDirectory() {
*/
@ParameterizedTest
@DisplayName("KeyStore Loader Positive Test")
@ValueSource(strings = {"legacy-valid", "hybrid-valid", "enhanced-valid", "enhanced-valid-no-agreement-key"})
@ValueSource(
strings = {
"legacy-valid",
"hybrid-valid",
"enhanced-valid",
"enhanced-valid-no-agreement-key",
"enhanced-valid-no-tss-key"
})
void keyStoreLoaderPositiveTest(final String directoryName)
throws IOException, KeyLoadingException, KeyStoreException {
final Path keyDirectory = testDataDirectory.resolve(directoryName);
Expand Down Expand Up @@ -137,6 +150,8 @@ void keyStoreLoaderPositiveTest(final String directoryName)
assertThat(keysAndCerts.sigCert()).isNotNull();
assertThat(keysAndCerts.agrKeyPair()).isNotNull();
assertThat(keysAndCerts.sigKeyPair()).isNotNull();
assertThat(keysAndCerts.privateTssEncryptionKey()).isNotNull();
assertThat(keysAndCerts.publicTssEncryptionKey()).isNotNull();
}

assertThat(addr.getSigPublicKey()).isNotNull();
Expand Down Expand Up @@ -195,6 +210,30 @@ void keyStoreLoaderNegativeCase2Test(final String directoryName) throws IOExcept
assertThatCode(loader::keysAndCerts).isInstanceOf(KeyLoadingException.class);
}

/**
* The KeyStore Loader Corrupt TSS Key Test is designed to test the case where the key store loader is able to scan
* the key directory, but the TSS key is corrupt.
*
* @throws IOException if an I/O error occurs during test setup.
*/
@Test
@DisplayName("KeyStore Loader Corrupt TSS Key Test")
void keyStoreLoaderNegativeCorruptTssKey() throws IOException {
final Path keyDirectory = testDataDirectory.resolve("enhanced-invalid-case-3");
derektriley marked this conversation as resolved.
Show resolved Hide resolved
final AddressBook addressBook = addressBook();
final EnhancedKeyStoreLoader loader = EnhancedKeyStoreLoader.using(addressBook, configure(keyDirectory));

assertThat(keyDirectory).exists().isDirectory().isReadable().isNotEmptyDirectory();

assertThat(loader).isNotNull();
assertThatCode(loader::migrate).doesNotThrowAnyException();
assertThatCode(loader::scan)
.as(
"Scan operation should throw KeyLoadingException when processing a corrupt TSS key in '%s'",
keyDirectory)
.isInstanceOf(KeyLoadingException.class);
}

/**
* A helper method used to load the {@code settings.txt} configuration file and override the default key directory
* path with the provided key directory path.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
AVS7ccxMt5screfvBWYJkUhm3SRhdkNxvsk8KcDMEXgl
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
Abp8DkXb98ee86gVDHynZTOSsOtLI2Y+trNzL+ERu2gO
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
AbkZoskhbWjGqnbAjDKYA3uBExxwoILiEqm9QOeyx3Yv
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
ATD1I7+Pyho6oh7fpzj2tQxTV9Vp2a3wSia9jsXw1bUI
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
AXtqyKZ3PTuSk3t4BY1Gcxiw6SifnuXzU1zO/Zmh7NUZ
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
AZMIRAUnkI9m6nh1vWWGD84hg76ZLxGyNSLk/c1WS5QS
-----END PRIVATE KEY-----
Loading
Loading