Skip to content

Commit

Permalink
Check required certificates before performing update
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrkob committed Oct 22, 2024
1 parent adb2652 commit 2106ab7
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat
InstallationManager installationManager = imf.create(serverHome, mavenOptions);

try (InputStream is = context.getAttachmentStream(CERT_FILE.resolveModelAttribute(context, operation).asInt())) {
TrustCertificate tc = installationManager.parseCA(is);
TrustCertificate tc = installationManager.parseCertificate(is);

ModelNode entry = new ModelNode();
entry.get(CERT_KEY_ID).set(tc.getKeyID());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati

InstMgrCertificateRemoveHandler certificateRemoveHandler = new InstMgrCertificateRemoveHandler(imService, imf);
resourceRegistration.registerOperationHandler(InstMgrCertificateRemoveHandler.DEFINITION, certificateRemoveHandler);

InstMgrUnacceptedCertificateHandler unacceptedcertificateHandler = new InstMgrUnacceptedCertificateHandler(imService, imf);
resourceRegistration.registerOperationHandler(InstMgrUnacceptedCertificateHandler.DEFINITION, unacceptedcertificateHandler);
}

@Override
Expand Down Expand Up @@ -408,7 +411,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat
InstallationManager installationManager = imf.create(serverHome, mavenOptions);

ModelNode mCertificates = new ModelNode().addEmptyList();
Collection<TrustCertificate> trustedCertificates = installationManager.listCA();
Collection<TrustCertificate> trustedCertificates = installationManager.listTrustedCertificates();
for (TrustCertificate tc : trustedCertificates) {
ModelNode entry = new ModelNode();
entry.get(InstMgrConstants.CERT_KEY_ID).set(tc.getKeyID());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright The WildFly Authors
* SPDX-License-Identifier: Apache-2.0
*/

package org.wildfly.core.instmgr;

import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FILE;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collection;

import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationDefinition;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
import org.jboss.as.controller.SimpleOperationDefinitionBuilder;
import org.jboss.as.controller.registry.OperationEntry;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.wildfly.installationmanager.MavenOptions;
import org.wildfly.installationmanager.spi.InstallationManager;
import org.wildfly.installationmanager.spi.InstallationManagerFactory;
import org.xnio.streams.Streams;

/**
* Operation handler to get the history of the installation manager changes, either artifacts or configuration metadata as
* channel changes.
*/
public class InstMgrUnacceptedCertificateHandler extends InstMgrOperationStepHandler {
public static final String OPERATION_NAME = "unaccepted-certificates";

protected static final AttributeDefinition OFFLINE = SimpleAttributeDefinitionBuilder.create(InstMgrConstants.OFFLINE, ModelType.BOOLEAN)
.setStorageRuntime()
.setDefaultValue(ModelNode.FALSE)
.setRequired(false)
.build();

public static final OperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(OPERATION_NAME, InstMgrResolver.RESOLVER)
.withFlags(OperationEntry.Flag.HOST_CONTROLLER_ONLY)
.setReplyType(ModelType.OBJECT)
.setRuntimeOnly()
.setReplyValueType(ModelType.OBJECT)
.addParameter(OFFLINE)
.build();

InstMgrUnacceptedCertificateHandler(InstMgrService imService, InstallationManagerFactory imf) {
super(imService, imf);
}

@Override
public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
context.addStep(new OperationStepHandler() {
@Override
public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
try {
final Path serverHome = imService.getHomeDir();
final Path controllerTempDir = imService.getControllerTempDir();
final boolean offline = OFFLINE.resolveModelAttribute(context, operation).asBoolean();
final MavenOptions mavenOptions = new MavenOptions(null, offline);
final InstallationManager installationManager = imf.create(serverHome, mavenOptions);

final Collection<InputStream> downloadedCerts = installationManager.downloadRequiredCertificates();

final ModelNode mCertificates = new ModelNode().addEmptyList();
int i=0;
for (InputStream is : downloadedCerts) {
final Path certFile = controllerTempDir.resolve("required-cert-" + i++ + ".crt");
try (FileOutputStream fos = new FileOutputStream(certFile.toFile())) {
Streams.copyStream(is, fos);
is.close();
}

final ModelNode entry = new ModelNode();
entry.get(CERT_FILE).set(certFile.toAbsolutePath().toString());
mCertificates.add(entry);
}

final ModelNode result = context.getResult();
result.set(mCertificates);
} catch (OperationFailedException | RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}, OperationContext.Stage.RUNTIME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public class AddCertificatesCommand extends AbstractInstMgrCommand {
@Option(name = "non-interactive")
private boolean nonInteractive;

public AddCertificatesCommand() {
}

public AddCertificatesCommand(File certFile, boolean nonInteractive) {
this.certFile = certFile;
this.nonInteractive = nonInteractive;
}

@Override
protected Operation buildOperation() {
final ModelNode op = new ModelNode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@

package org.wildfly.core.instmgr.cli;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FILE;
import static org.wildfly.core.instmgr.InstMgrConstants.OFFLINE;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.aesh.command.CommandDefinition;
import org.aesh.command.CommandException;
Expand All @@ -23,9 +31,12 @@
import org.jboss.as.cli.operation.ParsedCommandLine;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.Operation;
import org.jboss.as.controller.client.OperationBuilder;
import org.jboss.dmr.ModelNode;
import org.wildfly.core.cli.command.aesh.CLICommandInvocation;
import org.wildfly.core.instmgr.InstMgrCertificateParseHandler;
import org.wildfly.core.instmgr.InstMgrConstants;
import org.wildfly.core.instmgr.InstMgrUnacceptedCertificateHandler;

@CommandDefinition(name = "update", description = "Apply the latest available patches on a server instance.", activator = InstMgrActivator.class)
public class UpdateCommand extends AbstractInstMgrCommand {
Expand Down Expand Up @@ -68,6 +79,13 @@ public CommandResult execute(CLICommandInvocation commandInvocation) throws Comm
final Boolean optNoResolveLocalCache = cmdParser.hasProperty("--" + NO_RESOLVE_LOCAL_CACHE_OPTION) ? noResolveLocalCache : null;
final Boolean optUseDefaultLocalCache = cmdParser.hasProperty("--" + USE_DEFAULT_LOCAL_CACHE_OPTION) ? useDefaultLocalCache : null;

// call the download handler
Collection<Path> pendingCertificates = getPendingCertificates(ctx);
// call the import handler
if (!importPendingCertificates(pendingCertificates, ctx, commandInvocation)) {
return CommandResult.SUCCESS;
}

ListUpdatesAction.Builder listUpdatesCmdBuilder = new ListUpdatesAction.Builder()
.setNoResolveLocalCache(optNoResolveLocalCache)
.setUseDefaultLocalCache(optUseDefaultLocalCache)
Expand Down Expand Up @@ -146,6 +164,65 @@ public CommandResult execute(CLICommandInvocation commandInvocation) throws Comm
return CommandResult.SUCCESS;
}

private boolean importPendingCertificates(Collection<Path> pendingCertificates, CommandContext ctx, CLICommandInvocation commandInvocation) throws CommandException, InterruptedException {
commandInvocation.println("The update is configured to verify the integrity of updated components, but following certificates need to be trusted:");

for (Path pendingCertificate : pendingCertificates) {
final ModelNode modelNode = this.executeOp(buildParseOperation(pendingCertificate), ctx, this.host).get(RESULT);

commandInvocation.println("key-id: " + modelNode.get(InstMgrConstants.CERT_KEY_ID));
commandInvocation.println("fingerprint: " + modelNode.get(InstMgrConstants.CERT_FINGERPRINT));
commandInvocation.println("description: " + modelNode.get(InstMgrConstants.CERT_DESCRIPTION));
commandInvocation.println("");

}
final String input = commandInvocation.inputLine(new Prompt("Import these certificates y/N "));
if ("y".equals(input)) {
commandInvocation.print("Importing a trusted certificate");

for (Path pendingCertificate : pendingCertificates) {
new AddCertificatesCommand(pendingCertificate.toFile(), false).executeOp(ctx, this.host);
}

return true;
} else {
commandInvocation.print("Importing canceled.");
return false;
}
}

protected Operation buildParseOperation(Path pendingCertificate) {
final ModelNode op = new ModelNode();
final OperationBuilder operationBuilder = OperationBuilder.create(op);

op.get(OP).set(InstMgrCertificateParseHandler.DEFINITION.getName());
op.get(CERT_FILE).set(0);
operationBuilder.addFileAsAttachment(pendingCertificate.toFile());

return operationBuilder.build();
}

private Collection<Path> getPendingCertificates(CommandContext ctx) throws CommandException {
if (confirm || dryRun) {
// skip the check in non-interactive runs because the certificate cannot be accepted either way
// the update will fail if certificate is required and will print error message
return Collections.emptyList();
}

final ModelNode op = new ModelNode();

op.get(OP).set(InstMgrUnacceptedCertificateHandler.DEFINITION.getName());
op.get(OFFLINE).set(offline);

final ModelNode modelNode = executeOp(OperationBuilder.create(op).build(), ctx, this.host);

final List<ModelNode> paths = modelNode.get(RESULT).asListOrEmpty();
return paths.stream()
.map(n->n.get(CERT_FILE).asString())
.map(Path::of)
.collect(Collectors.toList());
}

private void printListUpdatesResult(CLICommandInvocation commandInvocation, List<ModelNode> changesMn) {
if (changesMn.isEmpty()) {
commandInvocation.println("No updates found");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ installation-manager.certificate-remove=Remove a certificate to verify installat
installation-manager.certificate-remove.key-id=The ID of a certificate to be removed from the list of certificates trusted to verify updated components.
installation-manager.certificates=Certificates used to verify components.
installation-manager.certificates.certificate.key-id=The ID of a certificate used to verify components.
installation-manager.unaccepted-certificates=Downloads certificates listed in the server channels, but not trusted yet.
installation-manager.unaccepted-certificates.offline=Only resolve certificates available locally.
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INCLUDE_RUNTIME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATIONS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_DESCRIPTION_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RECURSIVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESPONSE_HEADERS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.WRITE_ATTRIBUTE_OPERATION;
import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION;
Expand Down Expand Up @@ -1545,6 +1548,21 @@ public void removeNonExistentCustomPatch() throws IOException {
);
}

@Test
public void downloadUnacceptedCertificates() throws IOException {
TestInstallationManager.initialize();

PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME);
ModelNode op = Util.createEmptyOperation(InstMgrUnacceptedCertificateHandler.OPERATION_NAME, pathElements);

ModelNode rsp = getController().execute(op, null, null, null);
Assert.assertEquals(SUCCESS, rsp.get(OUTCOME).asString());

final String certText = Files.readString(Path.of(rsp.get(RESULT).asList().get(0).get(CERT_FILE).asString()));

Assert.assertEquals("test cert", certText);
}

/**
* Creates and upload a custom patch associated to the customPatchManifest passed as argument.
* It will use as Zip content the content available in the "test-repo-one" resource directory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
package org.wildfly.test.installationmanager;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
Expand Down Expand Up @@ -302,7 +303,7 @@ public Collection<FileConflict> verifyCandidate(Path candidatePath, CandidateTyp

@Override
public void acceptTrustedCertificates(InputStream certificate) throws Exception {
final TrustCertificate trustCertificate = parseCA(certificate);
final TrustCertificate trustCertificate = parseCertificate(certificate);

lstTrustCertificates.add(trustCertificate);
}
Expand All @@ -317,12 +318,12 @@ public void revokeTrustedCertificate(String keyID) throws Exception {
}

@Override
public Collection<TrustCertificate> listCA() throws Exception {
public Collection<TrustCertificate> listTrustedCertificates() throws Exception {
return Collections.unmodifiableCollection(lstTrustCertificates);
}

@Override
public TrustCertificate parseCA(InputStream certificate) throws Exception {
public TrustCertificate parseCertificate(InputStream certificate) throws Exception {
String keyId = null;
String fingerprint = null;
String description = null;
Expand All @@ -348,6 +349,13 @@ public TrustCertificate parseCA(InputStream certificate) throws Exception {
return new TrustCertificate(keyId, fingerprint, description, "TRUSTED");
}

@Override
public Collection<InputStream> downloadRequiredCertificates() throws Exception {
final String cert = "test cert";
final ByteArrayInputStream bais = new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8));
return List.of(bais);
}

public static void zipDir(Path inputFile, Path target) throws IOException {
try (FileOutputStream fos = new FileOutputStream(target.toFile()); ZipOutputStream zos = new ZipOutputStream(fos)) {
ZipEntry entry = new ZipEntry(inputFile.getFileName().toString());
Expand Down

0 comments on commit 2106ab7

Please sign in to comment.