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

chore: Consolidate key logic into fewer locations; reduce duplication #18237

Open
wants to merge 2 commits into
base: main
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
1 change: 1 addition & 0 deletions hedera-node/hapi-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ testModuleInfo {
requires("org.junit.jupiter.api")
requires("org.junit.jupiter.params")
requires("org.mockito")
requires("org.assertj.core")
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.node.app.hapi.utils.keys;

import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.Provider;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
Expand All @@ -15,47 +13,30 @@
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;

/**
* Minimal utility to read/write a single Ed25519 key from/to an encrypted PEM file.
*/
public final class Ed25519Utils {
private static final Provider BC_PROVIDER = new BouncyCastleProvider();
private static final Provider ED_PROVIDER = new EdDSASecurityProvider();

public static final EdDSANamedCurveSpec ED25519_PARAMS =
private static final EdDSANamedCurveSpec ED25519_PARAMS =
EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
private static final int ED25519_BYTE_LENGTH = 32;

static boolean isValidEd25519Key(@NonNull final Bytes key) {
return key.length() == ED25519_BYTE_LENGTH;
}

public static EdDSAPrivateKey readKeyFrom(final File pem, final String passphrase) {
return KeyUtils.readKeyFrom(pem, passphrase, ED_PROVIDER);
}

public static KeyPair readKeyPairFrom(final File pem, final String passphrase) {
return keyPairFrom(readKeyFrom(pem, passphrase));
}

public static EdDSAPrivateKey readKeyFrom(final String pemLoc, final String passphrase) {
return readKeyFrom(new File(pemLoc), passphrase);
}

public static EdDSAPrivateKey readKeyFrom(final File pem, final String passphrase) {
final var relocatedPem = KeyUtils.relocatedIfNotPresentInWorkingDir(pem);
try (final var in = new FileInputStream(relocatedPem)) {
final var decryptProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.setProvider(BC_PROVIDER)
.build(passphrase.toCharArray());
final var converter = new JcaPEMKeyConverter().setProvider(ED_PROVIDER);
try (final var parser = new PEMParser(new InputStreamReader(in))) {
final var encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) parser.readObject();
final var info = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptProvider);
return (EdDSAPrivateKey) converter.getPrivateKey(info);
}
} catch (final IOException | OperatorCreationException | PKCSException e) {
throw new IllegalArgumentException(e);
}
return KeyUtils.readKeyFrom(new File(pemLoc), passphrase, ED_PROVIDER);
}

public static byte[] extractEd25519PublicKey(@NonNull final EdDSAPrivateKey key) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.node.app.spi.key;
package com.hedera.node.app.hapi.utils.keys;

import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.base.Key;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.node.app.hapi.utils.keys;

import static com.hedera.node.app.hapi.utils.keys.Ed25519Utils.isValidEd25519Key;
import static com.hedera.node.app.hapi.utils.keys.Secp256k1Utils.isValidEcdsaSecp256k1Key;
import static com.hedera.node.app.hapi.utils.keys.Secp256k1Utils.isValidEvmAddress;

import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.KeyList;
import com.hedera.hapi.node.base.ThresholdKey;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.file.Path;
import java.security.DrbgParameters;
Expand All @@ -13,21 +25,34 @@
import java.security.Provider;
import java.security.SecureRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PKCS8Generator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;

/**
* Utility class for working with algorithm-agnostic cryptographic keys
*/
public final class KeyUtils {
public static final Provider BC_PROVIDER = new BouncyCastleProvider();
public static final Key IMMUTABILITY_SENTINEL_KEY =
Key.newBuilder().keyList(KeyList.DEFAULT).build();
public static final String TEST_CLIENTS_PREFIX = "hedera-node" + File.separator + "test-clients" + File.separator;

static final Provider BC_PROVIDER = new BouncyCastleProvider();

private static final int ENCRYPTOR_ITERATION_COUNT = 10_000;
private static final String RESOURCE_PATH_SEGMENT = "src/main/resource";

private KeyUtils() {
throw new UnsupportedOperationException("Utility Class");
}

private static final DrbgParameters.Instantiation DRBG_INSTANTIATION =
DrbgParameters.instantiation(256, DrbgParameters.Capability.RESEED_ONLY, null);

Expand All @@ -39,6 +64,39 @@ public static File relocatedIfNotPresentInWorkingDir(final File file) {
return relocatedIfNotPresentWithCurrentPathPrefix(file, RESOURCE_PATH_SEGMENT, TEST_CLIENTS_PREFIX);
}

public static File relocatedIfNotPresentWithCurrentPathPrefix(
final File file, final String firstSegmentToRelocate, final String newPathPrefix) {
if (!file.exists()) {
final var absPath = withDedupedHederaNodePathSegments(file.getAbsolutePath());
final var idx = absPath.indexOf(firstSegmentToRelocate);
if (idx == -1) {
return new File(absPath);
}
final var relocatedPath = newPathPrefix + absPath.substring(idx);
return new File(relocatedPath);
} else {
return file;
}
}

public static <T extends PrivateKey> T readKeyFrom(
final File pem, final String passphrase, final Provider pemKeyProvider) {
final var relocatedPem = KeyUtils.relocatedIfNotPresentInWorkingDir(pem);
try (final var in = new FileInputStream(relocatedPem)) {
final var decryptProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.setProvider(BC_PROVIDER)
.build(passphrase.toCharArray());
final var converter = new JcaPEMKeyConverter().setProvider(pemKeyProvider);
try (final var parser = new PEMParser(new InputStreamReader(in))) {
final var encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) parser.readObject();
final var info = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptProvider);
return (T) converter.getPrivateKey(info);
}
} catch (final IOException | OperatorCreationException | PKCSException e) {
throw new IllegalArgumentException(e);
}
}

public static void writeKeyTo(final PrivateKey key, final String pemLoc, final String passphrase) {
final var pem = new File(pemLoc);
try (final var out = new FileOutputStream(pem)) {
Expand All @@ -59,19 +117,110 @@ public static void writeKeyTo(final PrivateKey key, final String pemLoc, final S
}
}

public static File relocatedIfNotPresentWithCurrentPathPrefix(
final File file, final String firstSegmentToRelocate, final String newPathPrefix) {
if (!file.exists()) {
final var absPath = withDedupedHederaNodePathSegments(file.getAbsolutePath());
final var idx = absPath.indexOf(firstSegmentToRelocate);
if (idx == -1) {
return new File(absPath);
/**
* Checks if the given key is empty.
* For a KeyList type checks if the list is empty.
* For a ThresholdKey type checks if the list is empty.
* For an Ed25519 or EcdsaSecp256k1 type checks if there are zero bytes.
* @param pbjKey the key to check
* @return true if the key is empty, false otherwise
*/
public static boolean isEmpty(@Nullable final Key pbjKey) {
return isEmptyInternal(pbjKey, false);
}

/**
* Checks if the given key is empty.
* For a KeyList type checks if the list is empty.
* For a ThresholdKey type checks if the list is empty.
* For an Ed25519 or EcdsaSecp256k1 type checks if there are zero bytes.
* @param pbjKey the key to check
* @param honorImmutable if true, the key is NOT considered EMPTY if it is the IMMUTABILITY_SENTINEL_KEY
* @return true if the key is empty, false otherwise
*/
private static boolean isEmptyInternal(@Nullable final Key pbjKey, boolean honorImmutable) {
if (pbjKey == null) {
return true;
}
final var key = pbjKey.key();
if (key == null || Key.KeyOneOfType.UNSET.equals(key.kind())) {
return true;
}
if (honorImmutable && IMMUTABILITY_SENTINEL_KEY.equals(pbjKey)) {
return false;
}
if (pbjKey.hasKeyList()) {
final var keyList = (KeyList) key.value();
if (keyList.keys().isEmpty()) {
return true;
}
final var relocatedPath = newPathPrefix + absPath.substring(idx);
return new File(relocatedPath);
} else {
return file;
for (final var k : keyList.keys()) {
if (!isEmpty(k)) {
return false;
}
}
return true;
} else if (pbjKey.hasThresholdKey()) {
final var thresholdKey = (ThresholdKey) key.value();
if ((!thresholdKey.hasKeys() || thresholdKey.keys().keys().size() == 0)) {
return true;
}
for (final var k : thresholdKey.keys().keys()) {
if (!isEmpty(k)) {
return false;
}
}
return true;
} else if (pbjKey.hasEd25519()) {
return ((Bytes) key.value()).length() == 0;
} else if (pbjKey.hasEcdsaSecp256k1()) {
return ((Bytes) key.value()).length() == 0;
} else if (pbjKey.hasDelegatableContractId() || pbjKey.hasContractID()) {
return ((ContractID) key.value()).contractNumOrElse(0L) == 0
&& ((ContractID) key.value()).evmAddressOrElse(Bytes.EMPTY).length() == 0L;
}
// ECDSA_384 and RSA_3072 are not supported yet
return true;
}

/**
* Checks if the given key is valid. Based on the key type it checks the basic requirements
* for the key type.
* @param pbjKey the key to check
* @return true if the key is valid, false otherwise
*/
public static boolean isValid(@Nullable final Key pbjKey) {
if (isEmpty(pbjKey)) {
return false;
}
final var key = pbjKey.key();
if (pbjKey.hasKeyList()) {
for (Key keys : ((KeyList) key.value()).keys()) {
if (!isValid(keys)) {
return false;
}
}
return true;
} else if (pbjKey.hasThresholdKey()) {
final int length = ((ThresholdKey) key.value()).keys().keys().size();
final int threshold = ((ThresholdKey) key.value()).threshold();
boolean isKeyListValid = true;
for (Key keys : ((ThresholdKey) key.value()).keys().keys()) {
if (!isValid(keys)) {
isKeyListValid = false;
break;
}
}
return (threshold >= 1 && threshold <= length && isKeyListValid);
} else if (pbjKey.hasEd25519()) {
return isValidEd25519Key((Bytes) key.value());
} else if (pbjKey.hasEcdsaSecp256k1()) {
return isValidEcdsaSecp256k1Key((Bytes) key.value());
} else if (pbjKey.hasDelegatableContractId() || pbjKey.hasContractID()) {
return isValidEvmAddress((ContractID) key.value());
}
// ECDSA_384 and RSA_3072 are not supported yet
return true;
}

private static String withDedupedHederaNodePathSegments(@NonNull final String loc) {
Expand All @@ -86,8 +235,4 @@ private static String withDedupedHederaNodePathSegments(@NonNull final String lo
return loc;
}
}

private KeyUtils() {
throw new UnsupportedOperationException("Utility Class");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,51 @@
package com.hedera.node.app.hapi.utils.keys;

import static com.hedera.node.app.hapi.utils.keys.KeyUtils.BC_PROVIDER;
import static com.hedera.node.app.hapi.utils.keys.KeyUtils.relocatedIfNotPresentInWorkingDir;

import com.hedera.hapi.node.base.ContractID;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.hederahashgraph.api.proto.java.Key;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.interfaces.ECPrivateKey;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;

/**
* Useful methods for interacting with SECP256K1 ECDSA keys.
*/
public class Secp256k1Utils {
public static final int ECDSA_SECP256K1_COMPRESSED_KEY_LENGTH = 33;

private static final int EVM_ADDRESS_BYTE_LENGTH = 20;
private static final byte ODD_PARITY = (byte) 0x03;
private static final byte EVEN_PARITY = (byte) 0x02;

static boolean isValidEvmAddress(@NonNull final ContractID contractId) {
return contractId.contractNumOrElse(0L) > 0
|| contractId.evmAddressOrElse(Bytes.EMPTY).length() == EVM_ADDRESS_BYTE_LENGTH;
}

public static byte[] extractEcdsaPublicKey(final ECPrivateKey key) {
final ECPoint pointQ =
ECNamedCurveTable.getParameterSpec("secp256k1").getG().multiply(key.getS());
return pointQ.getEncoded(true);
}

public static byte[] getEvmAddressFromString(final Key key) {
return extractEcdsaPublicKey(key);
}

public static byte[] extractEcdsaPublicKey(final Key key) {
return key.getECDSASecp256K1().toByteArray();
}

public static ECPrivateKey readECKeyFrom(final File pem, final String passphrase) {
final var relocatedPem = relocatedIfNotPresentInWorkingDir(pem);
try (final var in = new FileInputStream(relocatedPem)) {
final var decryptProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.setProvider(BC_PROVIDER)
.build(passphrase.toCharArray());
final var converter = new JcaPEMKeyConverter().setProvider(BC_PROVIDER);
try (final var parser = new PEMParser(new InputStreamReader(in))) {
final var encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) parser.readObject();
final var info = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptProvider);
return (ECPrivateKey) converter.getPrivateKey(info);
}
} catch (final IOException | OperatorCreationException | PKCSException e) {
throw new IllegalArgumentException(e);
}
return KeyUtils.readKeyFrom(pem, passphrase, BC_PROVIDER);
}

static boolean isValidEcdsaSecp256k1Key(@NonNull final Bytes key) {
return key.length() == ECDSA_SECP256K1_COMPRESSED_KEY_LENGTH
&& (key.getByte(0) == EVEN_PARITY || key.getByte(0) == ODD_PARITY);
}
}
Loading
Loading