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: Do stricter validation of X.509 gossip cert in DAB transactions #16666

Merged
merged 2 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import static com.hedera.hapi.node.base.ResponseCodeEnum.IP_FQDN_CANNOT_BE_SET_FOR_SAME_ENDPOINT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.KEY_REQUIRED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SERVICE_ENDPOINTS_EXCEEDED_LIMIT;
import static com.hedera.node.app.service.addressbook.AddressBookHelper.readCertificatePemFile;
import static com.hedera.node.app.service.addressbook.AddressBookHelper.writeCertificatePemFile;
import static com.hedera.node.app.spi.key.KeyUtils.isEmpty;
import static com.hedera.node.app.spi.key.KeyUtils.isValid;
import static com.hedera.node.app.spi.validation.Validations.validateAccountID;
Expand All @@ -47,10 +49,8 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
Expand Down Expand Up @@ -175,15 +175,22 @@ private void validateEndpoint(@NonNull final ServiceEndpoint endpoint, @NonNull
}

/**
* Validate the Bytes is a real X509Certificate bytes.
* @param certBytes the Bytes to validate
* Validates the given bytes encode an X509 certificate can be serialized and deserialized from
* PEM format to recover a usable certificate.
* @param x509CertBytes the bytes to validate
* @throws PreCheckException if the certificate is invalid
*/
public static void validateX509Certificate(@NonNull Bytes certBytes) throws PreCheckException {
public static void validateX509Certificate(@NonNull final Bytes x509CertBytes) throws PreCheckException {
try {
final var cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(certBytes.toByteArray()));
} catch (final CertificateException e) {
// Serialize the given bytes to a PEM file just as we would on a PREPARE_UPGRADE
final var baos = new ByteArrayOutputStream();
writeCertificatePemFile(x509CertBytes.toByteArray(), baos);
// Deserialize an X509 certificate from the resulting PEM file
final var bais = new ByteArrayInputStream(baos.toByteArray());
final var cert = readCertificatePemFile(bais);
// And check its validity for completeness
cert.checkValidity();
} catch (Exception ignore) {
throw new PreCheckException(INVALID_GOSSIP_CA_CERTIFICATE);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hedera.node.app.service.addressbook.impl.validators;

import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_GOSSIP_CA_CERTIFICATE;
import static com.hedera.node.app.service.addressbook.AddressBookHelper.writeCertificatePemFile;
import static com.hedera.node.app.service.addressbook.impl.test.handlers.AddressBookTestBase.generateX509Certificates;
import static com.hedera.node.app.service.addressbook.impl.validators.AddressBookValidator.validateX509Certificate;
import static org.junit.jupiter.api.Assertions.*;

import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class AddressBookValidatorTest {
private static X509Certificate x509Cert;

@BeforeAll
static void beforeAll() {
x509Cert = generateX509Certificates(1).getFirst();
}

@Test
void encodedCertPassesValidation() {
assertDoesNotThrow(() -> validateX509Certificate(Bytes.wrap(x509Cert.getEncoded())));
}

@Test
void utf8EncodingOfX509PemFailsValidation() throws CertificateEncodingException, IOException {
final var baos = new ByteArrayOutputStream();
writeCertificatePemFile(x509Cert.getEncoded(), baos);
final var e =
assertThrows(PreCheckException.class, () -> validateX509Certificate(Bytes.wrap(baos.toByteArray())));
assertEquals(INVALID_GOSSIP_CA_CERTIFICATE, e.responseCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateException;
Expand Down Expand Up @@ -71,19 +72,28 @@ public static long getNextNodeID(@NonNull final ReadableNodeStore nodeStore) {
/**
* Write the Certificate to a pem file.
* @param pemFile to write
* @param encodes Certificate encoded byte[]
* @param x509Encoding Certificate encoded byte[]
* @throws IOException if an I/O error occurs while writing the file
*/
public static void writeCertificatePemFile(@NonNull final Path pemFile, @NonNull final byte[] encodes)
public static void writeCertificatePemFile(@NonNull final Path pemFile, @NonNull final byte[] x509Encoding)
throws IOException {
Objects.requireNonNull(pemFile, "pemFile must not be null");
Objects.requireNonNull(encodes, "cert must not be null");
writeCertificatePemFile(x509Encoding, new FileOutputStream(pemFile.toFile()));
}

final PemObject pemObj = new PemObject("CERTIFICATE", encodes);
try (final var f = new FileOutputStream(pemFile.toFile());
final var out = new OutputStreamWriter(f);
final PemWriter writer = new PemWriter(out)) {
writer.writeObject(pemObj);
/**
* Given an X509 encoded certificate, writes it as a PEM to the given output stream.
*
* @param x509Encoding the X509 encoded certificate
* @param out the output stream to write to
* @throws IOException if an I/O error occurs while writing the PEM
*/
public static void writeCertificatePemFile(@NonNull final byte[] x509Encoding, @NonNull final OutputStream out)
throws IOException {
requireNonNull(x509Encoding);
requireNonNull(out);
try (final var writer = new OutputStreamWriter(out);
final PemWriter pemWriter = new PemWriter(writer)) {
pemWriter.writeObject(new PemObject("CERTIFICATE", x509Encoding));
}
}

Expand All @@ -96,22 +106,26 @@ public static void writeCertificatePemFile(@NonNull final Path pemFile, @NonNull
*/
public static X509Certificate readCertificatePemFile(@NonNull final Path pemFile)
throws IOException, CertificateException {
Objects.requireNonNull(pemFile, "pemFile must not be null");
X509Certificate cert = null;
Object entry;
try (final PEMParser parser =
new PEMParser(new InputStreamReader(Files.newInputStream(pemFile), StandardCharsets.UTF_8))) {
while ((entry = parser.readObject()) != null) {
if (entry instanceof X509CertificateHolder ch) {
cert = new JcaX509CertificateConverter().getCertificate(ch);
break;
} else {
throw new CertificateException(
"Not X509 Certificate, it is " + entry.getClass().getSimpleName());
}
return readCertificatePemFile(Files.newInputStream(pemFile));
}

/**
* Reads a PEM-encoded X509 certificate from the given input stream.
* @param in the input stream to read from
* @return the X509Certificate
* @throws IOException if an I/O error occurs while reading the certificate
* @throws CertificateException if the file does not contain a valid X509Certificate
*/
public static X509Certificate readCertificatePemFile(@NonNull final InputStream in)
throws IOException, CertificateException {
requireNonNull(in);
try (final var parser = new PEMParser(new InputStreamReader(in))) {
final var entry = parser.readObject();
if (!(entry instanceof X509CertificateHolder holder)) {
throw new CertificateException();
}
return new JcaX509CertificateConverter().getCertificate(holder);
}
return cert;
}

/**
Expand Down
Loading