diff --git a/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java b/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java index 04f524ffb84..ba209e5746c 100644 --- a/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java +++ b/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java @@ -84,6 +84,7 @@ public class ModelDescriptionConstants { public static final String CAPABILITY_REFERENCE = "capability-reference"; public static final String CAPABILITY_REFERENCE_PATTERN_ELEMENTS = "capability-reference-pattern-elements"; public static final String CAPABILITY_REGISTRY = "capability-registry"; + public static final String CERTIFICATE_INFO = "certificate-info"; public static final String CHILD_TYPE = "child-type"; public static final String CHILDREN = "children"; public static final String CLASSIFICATION = "classification"; diff --git a/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/installation-manager/main/module.xml b/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/installation-manager/main/module.xml index 920f26e0fe4..61d41c6b4f3 100644 --- a/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/installation-manager/main/module.xml +++ b/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/installation-manager/main/module.xml @@ -25,5 +25,6 @@ + diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateImportHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateImportHandler.java new file mode 100644 index 00000000000..67aaff0e50e --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateImportHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTACHED_STREAMS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FILESYSTEM_PATH; +import static org.wildfly.core.instmgr.InstMgrConstants.CERTIFICATE_CONTENT; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +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.core.instmgr.logging.InstMgrLogger; +import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.installationmanager.spi.InstallationManager; +import org.wildfly.installationmanager.spi.InstallationManagerFactory; + +/** + * Operation handler to import a certificate used to validate components installed during updates. + * The certificate has to be an ASCII-armored PGP public certificate. + */ +public class InstMgrCertificateImportHandler extends InstMgrOperationStepHandler { + public static final String OPERATION_NAME = "certificate-import"; + + protected static final AttributeDefinition CERT_FILE = SimpleAttributeDefinitionBuilder.create(InstMgrConstants.CERT_FILE, ModelType.INT) + .setStorageRuntime() + .setRequired(false) + .addArbitraryDescriptor(FILESYSTEM_PATH, ModelNode.TRUE) + .addArbitraryDescriptor(ATTACHED_STREAMS, ModelNode.TRUE) + .build(); + protected static final AttributeDefinition CERT_CONTENT = SimpleAttributeDefinitionBuilder.create(CERTIFICATE_CONTENT, ModelType.STRING) + .setStorageRuntime() + .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(CERT_FILE) + .build(); + + InstMgrCertificateImportHandler(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 { + Path serverHome = imService.getHomeDir(); + MavenOptions mavenOptions = new MavenOptions(null, false); + InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + final Integer streamIndex = CERT_FILE.resolveModelAttribute(context, operation).asIntOrNull(); + final String certificateContent = CERT_CONTENT.resolveModelAttribute(context, operation).asStringOrNull(); + + if (streamIndex != null && certificateContent != null) { + throw InstMgrLogger.ROOT_LOGGER.mutuallyExclusiveOptions(CERT_FILE.getName(), CERT_CONTENT.getName()); + } + + if (streamIndex != null) { + try (InputStream is = context.getAttachmentStream(streamIndex)) { + installationManager.acceptTrustedCertificates(is); + } + } else if (certificateContent != null) { + try (InputStream is = new ByteArrayInputStream(certificateContent.getBytes(StandardCharsets.UTF_8))) { + installationManager.acceptTrustedCertificates(is); + } + } else { + // throw exception + } + } catch (OperationFailedException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java new file mode 100644 index 00000000000..73dfe3a0e52 --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTACHED_STREAMS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FILESYSTEM_PATH; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FINGERPRINT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_STATUS; + +import java.io.InputStream; +import java.nio.file.Path; + +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.TrustCertificate; +import org.wildfly.installationmanager.spi.InstallationManager; +import org.wildfly.installationmanager.spi.InstallationManagerFactory; + +/** + * Operation handler to parse the certificate file and read the certificate information. Expects am ASCII-armored PGP public key. + */ +public class InstMgrCertificateParseHandler extends InstMgrOperationStepHandler { + public static final String OPERATION_NAME = "certificate-parse"; + + protected static final AttributeDefinition CERT_FILE = SimpleAttributeDefinitionBuilder.create(InstMgrConstants.CERT_FILE, ModelType.INT) + .setStorageRuntime() + .setRequired(true) + .addArbitraryDescriptor(FILESYSTEM_PATH, ModelNode.TRUE) + .addArbitraryDescriptor(ATTACHED_STREAMS, ModelNode.TRUE) + .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(CERT_FILE) + .build(); + + InstMgrCertificateParseHandler(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 { + Path serverHome = imService.getHomeDir(); + MavenOptions mavenOptions = new MavenOptions(null, false); + InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + try (InputStream is = context.getAttachmentStream(CERT_FILE.resolveModelAttribute(context, operation).asInt())) { + final TrustCertificate tc = installationManager.parseCertificate(is); + + final ModelNode entry = new ModelNode(); + entry.get(CERT_KEY_ID).set(tc.getKeyID()); + entry.get(CERT_FINGERPRINT).set(tc.getFingerprint()); + entry.get(CERT_DESCRIPTION).set(tc.getDescription()); + entry.get(CERT_STATUS).set(tc.getStatus()); + context.getResult().set(entry); + } + } catch (OperationFailedException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateRemoveHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateRemoveHandler.java new file mode 100644 index 00000000000..e8d6ae2105d --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateRemoveHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr; + +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; + +import java.nio.file.Path; + +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; + +/** + * Operation handler to remove one of imported component certificates. The certificate is identified by it's ID (in hex format). + * Once the certificate is removed, the user will have to re-import it to apply and updates including a component + * signed by that certificate. + */ +public class InstMgrCertificateRemoveHandler extends InstMgrOperationStepHandler { + public static final String OPERATION_NAME = "certificate-remove"; + + protected static final AttributeDefinition KEY_ID = SimpleAttributeDefinitionBuilder.create(CERT_KEY_ID, ModelType.STRING) + .setStorageRuntime() + .setRequired(true) + .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(KEY_ID) + .build(); + + InstMgrCertificateRemoveHandler(InstMgrService imService, InstallationManagerFactory imf) { + super(imService, imf); + } + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + final String keyId = KEY_ID.resolveModelAttribute(context, operation).asString(); + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + try { + Path serverHome = imService.getHomeDir(); + MavenOptions mavenOptions = new MavenOptions(null, false); + InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + installationManager.revokeTrustedCertificate(keyId); + } catch (OperationFailedException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrConstants.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrConstants.java index 2abebd93f1f..dccd9f4a058 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrConstants.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrConstants.java @@ -15,6 +15,14 @@ public interface InstMgrConstants { Path PREPARED_SERVER_SUBPATH = Paths.get("installation-manager") .resolve("prepared-server"); + String CERT_DESCRIPTION = "description"; + String CERT_FINGERPRINT = "fingerprint"; + String CERT_KEY_ID = "key-id"; + String CERT_STATUS = "status"; + String CERT_FILE = "cert-file"; + String CERTIFICATE = "certificate"; + String CERTIFICATE_CONTENT = "certificate-content"; + String CERTIFICATES = "certificates"; String CHANNEL = "channel"; String CHANNELS = "channels"; String CHANNEL_NAME = "name"; diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrListUpdatesHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrListUpdatesHandler.java index efaf649265b..9eb5da7d3ad 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrListUpdatesHandler.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrListUpdatesHandler.java @@ -27,6 +27,7 @@ import org.wildfly.core.instmgr.logging.InstMgrLogger; import org.wildfly.installationmanager.ArtifactChange; import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.installationmanager.MissingSignatureException; import org.wildfly.installationmanager.Repository; import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.InstallationManagerFactory; @@ -177,6 +178,8 @@ public void handleResult(OperationContext.ResultAction resultAction, OperationCo throw new OperationFailedException(e.getLocalizedMessage()); } catch (OperationFailedException | RuntimeException e) { throw e; + } catch (MissingSignatureException e) { + throw InstMgrLogger.ROOT_LOGGER.componentSignedWithUnknownCertificate(e.getDescription(), e); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareRevertHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareRevertHandler.java index eaffed1b9bc..05b89fb3fea 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareRevertHandler.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareRevertHandler.java @@ -26,6 +26,7 @@ import org.jboss.dmr.ModelType; import org.wildfly.core.instmgr.logging.InstMgrLogger; import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.installationmanager.MissingSignatureException; import org.wildfly.installationmanager.Repository; import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.InstallationManagerFactory; @@ -147,6 +148,8 @@ public void execute(OperationContext context, ModelNode operation) throws Operat throw new OperationFailedException(e.getLocalizedMessage()); } catch (OperationFailedException | RuntimeException e) { throw e; + } catch (MissingSignatureException e) { + throw InstMgrLogger.ROOT_LOGGER.componentSignedWithUnknownCertificate(e.getDescription(), e); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareUpdateHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareUpdateHandler.java index ee1aad22d4f..a3cd98db673 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareUpdateHandler.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrPrepareUpdateHandler.java @@ -29,6 +29,7 @@ import org.jboss.dmr.ModelType; import org.wildfly.core.instmgr.logging.InstMgrLogger; import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.installationmanager.MissingSignatureException; import org.wildfly.installationmanager.Repository; import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.InstallationManagerFactory; @@ -189,6 +190,8 @@ public void execute(OperationContext context, ModelNode operation) throws Operat throw new OperationFailedException(e.getLocalizedMessage()); } catch (OperationFailedException | RuntimeException e) { throw e; + } catch (MissingSignatureException e) { + throw InstMgrLogger.ROOT_LOGGER.componentSignedWithUnknownCertificate(e.getDescription(), e); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java index 890cb8c088a..66fdfc562f0 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java @@ -39,6 +39,7 @@ import org.wildfly.installationmanager.Channel; import org.wildfly.installationmanager.MavenOptions; import org.wildfly.installationmanager.Repository; +import org.wildfly.installationmanager.TrustCertificate; import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.InstallationManagerFactory; @@ -86,6 +87,12 @@ class InstMgrResourceDefinition extends SimpleResourceDefinition { .setRequired(true) .build(); + private static final AttributeDefinition KEY_ID = new SimpleAttributeDefinitionBuilder(InstMgrConstants.CERT_KEY_ID, ModelType.STRING) + .setStorageRuntime() + .setRuntimeServiceNotRequired() + .setRequired(true) + .build(); + private static final AttributeDefinition MANIFEST_GAV = new SimpleAttributeDefinitionBuilder(InstMgrConstants.MANIFEST_GAV, ModelType.STRING) .setAlternatives(InstMgrConstants.MANIFEST_URL) .setStorageRuntime() @@ -115,11 +122,22 @@ class InstMgrResourceDefinition extends SimpleResourceDefinition { .setSuffix("channel") .build(); + private static final ObjectTypeAttributeDefinition CERTIFICATE = ObjectTypeAttributeDefinition.create(InstMgrConstants.CERTIFICATE, KEY_ID) + .setStorageRuntime() + .setRuntimeServiceNotRequired() + .setRequired(true) + .setSuffix("certificate") + .build(); + private static final AttributeDefinition CHANNELS = ObjectListAttributeDefinition.Builder.of(InstMgrConstants.CHANNELS, CHANNEL) .setStorageRuntime() .setRuntimeServiceNotRequired() .build(); + private static final AttributeDefinition CERTIFICATES = ObjectListAttributeDefinition.Builder.of("certificates", CERTIFICATE) + .setStorageRuntime() + .setRuntimeServiceNotRequired() + .build(); public static PathElement getPath(String name) { return PathElement.pathElement(CORE_SERVICE, name); } @@ -167,11 +185,24 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati InstMgrCustomPatchRemoveHandler customPatchRemoveHandler = new InstMgrCustomPatchRemoveHandler(imService, imf); resourceRegistration.registerOperationHandler(customPatchRemoveHandler.DEFINITION, customPatchRemoveHandler); + + InstMgrCertificateParseHandler certificateParseHandler = new InstMgrCertificateParseHandler(imService, imf); + resourceRegistration.registerOperationHandler(InstMgrCertificateParseHandler.DEFINITION, certificateParseHandler); + + InstMgrCertificateImportHandler certificateImportHandler = new InstMgrCertificateImportHandler(imService, imf); + resourceRegistration.registerOperationHandler(InstMgrCertificateImportHandler.DEFINITION, certificateImportHandler); + + InstMgrCertificateRemoveHandler certificateRemoveHandler = new InstMgrCertificateRemoveHandler(imService, imf); + resourceRegistration.registerOperationHandler(InstMgrCertificateRemoveHandler.DEFINITION, certificateRemoveHandler); + + InstMgrUnacceptedCertificateHandler unacceptedcertificateHandler = new InstMgrUnacceptedCertificateHandler(imService, imf); + resourceRegistration.registerOperationHandler(InstMgrUnacceptedCertificateHandler.DEFINITION, unacceptedcertificateHandler); } @Override public void registerAttributes(ManagementResourceRegistration resourceRegistration) { resourceRegistration.registerReadWriteAttribute(CHANNELS, new ReadHandler(), new WriteHandler()); + resourceRegistration.registerReadOnlyAttribute(CERTIFICATES, new CertReadHandler()); } private class WriteHandler implements OperationStepHandler { @@ -364,4 +395,38 @@ public void validateParameter(String parameterName, ModelNode value) throws Oper } } } + + private class CertReadHandler implements OperationStepHandler { + @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 ModelNode result = context.getResult(); + Path serverHome = imService.getHomeDir(); + + MavenOptions mavenOptions = new MavenOptions(null, false); + InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + ModelNode mCertificates = new ModelNode().addEmptyList(); + Collection trustedCertificates = installationManager.listTrustedCertificates(); + for (TrustCertificate tc : trustedCertificates) { + ModelNode entry = new ModelNode(); + entry.get(InstMgrConstants.CERT_KEY_ID).set(tc.getKeyID()); + entry.get(InstMgrConstants.CERT_FINGERPRINT).set(tc.getFingerprint()); + entry.get(InstMgrConstants.CERT_DESCRIPTION).set(tc.getDescription()); + entry.get(InstMgrConstants.CERT_STATUS).set(tc.getStatus()); + mCertificates.add(entry); + } + result.set(mCertificates); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } + } } diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java new file mode 100644 index 00000000000..b1d4e70b8e3 --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java @@ -0,0 +1,114 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CERTIFICATE_INFO; +import static org.wildfly.core.instmgr.InstMgrConstants.CERTIFICATE_CONTENT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FINGERPRINT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_STATUS; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +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.TrustCertificate; +import org.wildfly.installationmanager.spi.InstallationManager; +import org.wildfly.installationmanager.spi.InstallationManagerFactory; + +/** + * Operation handler to get the certificates potentially required by the update or revert operation. Those certificates + * are listed by the channel and may be used to sign the components included in the channel. + */ +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 boolean offline = OFFLINE.resolveModelAttribute(context, operation).asBoolean(); + final MavenOptions mavenOptions = new MavenOptions(null, offline); + final InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + final Collection downloadedCerts = installationManager.downloadRequiredCertificates(); + + final ModelNode mCertificates = new ModelNode().addEmptyList(); + for (InputStream is : downloadedCerts) { + final ModelNode mCert = new ModelNode(); + try (BufferedInputStream bis = new BufferedInputStream(is)) { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for (int result = bis.read(); result != -1; result = bis.read()) { + buf.write((byte) result); + } + + final String certContent = buf.toString(StandardCharsets.UTF_8); + mCert.get(CERTIFICATE_CONTENT).set(certContent); + try (final ByteArrayInputStream bais = new ByteArrayInputStream(certContent.getBytes(StandardCharsets.UTF_8))) { + final TrustCertificate tc = installationManager.parseCertificate(bais); + final ModelNode info = new ModelNode(); + info.get(CERT_KEY_ID).set(tc.getKeyID()); + info.get(CERT_FINGERPRINT).set(tc.getFingerprint()); + info.get(CERT_DESCRIPTION).set(tc.getDescription()); + info.get(CERT_STATUS).set(tc.getStatus()); + context.getResult().set(info); + + mCert.get(CERTIFICATE_INFO).set(info); + } + } + + + mCertificates.add(mCert); + } + + final ModelNode result = context.getResult(); + result.set(mCertificates); + } catch (OperationFailedException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AbstractInstMgrCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AbstractInstMgrCommand.java index 578f77b9aeb..85c6d3cdad2 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AbstractInstMgrCommand.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AbstractInstMgrCommand.java @@ -61,6 +61,10 @@ public abstract class AbstractInstMgrCommand implements Command { public static final String COMMAND_NAME = "installer"; diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/ListCertificatesCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/ListCertificatesCommand.java new file mode 100644 index 00000000000..c2873e67ffe --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/ListCertificatesCommand.java @@ -0,0 +1,93 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr.cli; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FINGERPRINT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_STATUS; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.aesh.command.CommandDefinition; +import org.aesh.command.CommandException; +import org.aesh.command.CommandResult; +import org.jboss.as.cli.CommandContext; +import org.jboss.as.cli.Util; +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.InstMgrConstants; + +@CommandDefinition(name = "certificates-list", description = "List trusted component certificates.", activator = InstMgrActivator.class) +public class ListCertificatesCommand extends AbstractInstMgrCommand { + + @Override + protected Operation buildOperation() { + final ModelNode op = new ModelNode(); + op.get(Util.OPERATION).set(Util.READ_RESOURCE); + op.get(Util.INCLUDE_RUNTIME).set(true); + + return OperationBuilder.create(op, true).build(); + } + + @Override + public CommandResult execute(CLICommandInvocation commandInvocation) throws CommandException, InterruptedException { + final CommandContext ctx = commandInvocation.getCommandContext(); + final ModelControllerClient client = ctx.getModelControllerClient(); + if (client == null) { + ctx.printLine("You are disconnected at the moment. Type 'connect' to connect to the server or 'help' for the list of supported commands."); + return CommandResult.FAILURE; + } + + ModelNode response = this.executeOp(commandInvocation.getCommandContext(), this.host); + ModelNode result = response.get(Util.RESULT); + List certificatesMn = result.get(InstMgrConstants.CERTIFICATES).asListOrEmpty(); + + if (certificatesMn.isEmpty()) { + ctx.printLine("No component certificates have been trusted for this installation."); + + return CommandResult.SUCCESS; + } + + ctx.printLine("-------"); + for (ModelNode certificate : certificatesMn) { + ctx.printLine("key ID " + certificate.get(CERT_KEY_ID).asString()); + ctx.printLine("fingerprint " + certificate.get(CERT_FINGERPRINT).asString()); + ctx.printLine("description " + certificate.get(CERT_DESCRIPTION).asString()); + ctx.printLine("status " + certificate.get(CERT_STATUS).asString()); + ctx.printLine("-------"); + } + return CommandResult.SUCCESS; + } + + /** + * Returns a Set with the current channel names available on the server installation + * + * @param ctx + * @param host + * @return + * @throws CommandException + */ + Set getAllChannelNames(CommandContext ctx, String host) throws CommandException { + final Set existingChannelNames = new HashSet<>(); + ModelNode listCmdResponse = this.executeOp(ctx, host); + if (listCmdResponse.hasDefined(RESULT)) { + ModelNode result = listCmdResponse.get(RESULT); + List channels = result.get(InstMgrConstants.CHANNELS).asListOrEmpty(); + for (ModelNode channel : channels) { + existingChannelNames.add(channel.get(NAME).asString()); + } + } + return existingChannelNames; + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/RemoveCertificatesCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/RemoveCertificatesCommand.java new file mode 100644 index 00000000000..e33ccaa8460 --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/RemoveCertificatesCommand.java @@ -0,0 +1,45 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr.cli; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; + +import org.aesh.command.CommandDefinition; +import org.aesh.command.CommandException; +import org.aesh.command.CommandResult; +import org.aesh.command.option.Option; +import org.jboss.as.cli.CommandContext; +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.InstMgrCertificateRemoveHandler; + +@CommandDefinition(name = "certificates-remove", description = "Removes a trusted component certificate.", activator = InstMgrActivator.class) +public class RemoveCertificatesCommand extends AbstractInstMgrCommand { + + @Option(name = CERT_KEY_ID, required = true) + private String keyId; + @Override + protected Operation buildOperation() { + final ModelNode op = new ModelNode(); + + op.get(OP).set(InstMgrCertificateRemoveHandler.DEFINITION.getName()); + op.get(CERT_KEY_ID).set(keyId); + + return OperationBuilder.create(op).build(); + } + + @Override + public CommandResult execute(CLICommandInvocation commandInvocation) throws CommandException, InterruptedException { + final CommandContext ctx = commandInvocation.getCommandContext(); + + this.executeOp(ctx, this.host); + + return CommandResult.SUCCESS; + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java index 52ebf8993db..0707526283a 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java @@ -5,9 +5,17 @@ package org.wildfly.core.instmgr.cli; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CERTIFICATE_INFO; +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.CERTIFICATE_CONTENT; +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 org.aesh.command.CommandDefinition; @@ -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.InstMgrCertificateImportHandler; 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 { @@ -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 pendingCertificates = getPendingCertificates(ctx); + // call the import handler + if (!pendingCertificates.isEmpty() && !importPendingCertificates(pendingCertificates, ctx, commandInvocation)) { + return CommandResult.SUCCESS; + } + ListUpdatesAction.Builder listUpdatesCmdBuilder = new ListUpdatesAction.Builder() .setNoResolveLocalCache(optNoResolveLocalCache) .setUseDefaultLocalCache(optUseDefaultLocalCache) @@ -146,6 +164,61 @@ public CommandResult execute(CLICommandInvocation commandInvocation) throws Comm return CommandResult.SUCCESS; } + private boolean importPendingCertificates(Collection 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 (ModelNode pendingCertificate : pendingCertificates) { + final ModelNode certificateInfo = pendingCertificate.get(CERTIFICATE_INFO); + + commandInvocation.println("key-id: " + certificateInfo.get(InstMgrConstants.CERT_KEY_ID)); + commandInvocation.println("fingerprint: " + certificateInfo.get(InstMgrConstants.CERT_FINGERPRINT)); + commandInvocation.println("description: " + certificateInfo.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 (ModelNode pendingCertificate : pendingCertificates) { + this.executeOp(buildImportOperation(pendingCertificate.get(CERTIFICATE_CONTENT).asString()), ctx, host); + } + + return true; + } else { + commandInvocation.print("Importing canceled."); + return false; + } + } + + protected Operation buildImportOperation(String certificateContent) { + final ModelNode op = new ModelNode(); + final OperationBuilder operationBuilder = OperationBuilder.create(op); + + op.get(OP).set(InstMgrCertificateImportHandler.DEFINITION.getName()); + op.get(CERTIFICATE_CONTENT).set(certificateContent); + + return operationBuilder.build(); + } + + private Collection 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); + + // certificate-content & certificate-info + return modelNode.get(RESULT).asListOrEmpty(); + } + private void printListUpdatesResult(CLICommandInvocation commandInvocation, List changesMn) { if (changesMn.isEmpty()) { commandInvocation.println("No updates found"); diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/logging/InstMgrLogger.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/logging/InstMgrLogger.java index fa9d6639bc7..6e0a8a23442 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/logging/InstMgrLogger.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/logging/InstMgrLogger.java @@ -13,6 +13,7 @@ import org.jboss.as.controller.OperationFailedException; import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Cause; import org.jboss.logging.annotations.LogMessage; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; @@ -105,6 +106,11 @@ public interface InstMgrLogger extends BasicLogger { @Message(id = 25, value = "Cannot report installation channels: '%s'") void failedToFindInstallationChannels(Exception failure); + @Message(id = 26, value = "One of the signatures in the update is signed by an unknown public key '%s'. Please import the key using import operation and try again.") + OperationFailedException componentSignedWithUnknownCertificate(String keyID, @Cause Throwable cause); + + @Message(id = 27, value = "You cannot use the '%s' option with the '%s' option because they are mutually exclusive.") + OperationFailedException mutuallyExclusiveOptions(String optionName1, String optionName2); //////////////////////////////////////////////// // Messages without IDs diff --git a/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties b/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties index 4b6c3af1692..142353efdbf 100644 --- a/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties +++ b/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties @@ -82,3 +82,14 @@ installation-manager.custom-patch.upload-custom-patch.custom-patch-file=A custom installation-manager.custom-patch.remove-custom-patch=Removes a custom patch from the base server. It also removes the channel that provides this custom patch and unsubscribes the installation from this channel. installation-manager.custom-patch.remove-custom-patch.manifest=Channel Manifest maven coordinates associated with the custom patch. Expected format is Maven GA (GroupId:ArtifactId). +installation-manager.certificate-list=Lists certificates used to verify components. +installation-manager.certificate-parse=Parses a certificate +installation-manager.certificate-parse.cert-file=A certificate file +installation-manager.certificate-import=Add a certificate to verify installation components. +installation-manager.certificate-import.cert-file=Path to the certificate that will be used to verify updated components. +installation-manager.certificate-remove=Remove a certificate to verify installation components. +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. diff --git a/installation-manager/src/main/resources/org/wildfly/core/instmgr/cli/command_resources.properties b/installation-manager/src/main/resources/org/wildfly/core/instmgr/cli/command_resources.properties index f339861ac3f..f513af56f35 100644 --- a/installation-manager/src/main/resources/org/wildfly/core/instmgr/cli/command_resources.properties +++ b/installation-manager/src/main/resources/org/wildfly/core/instmgr/cli/command_resources.properties @@ -194,4 +194,28 @@ installer.remove-custom-patch.description=\ installer.remove-custom-patch.option.manifest.description=\ Location of the channel manifest artifact associated with custom patch that will be removed. A manifest defines the versions and artifacts that will be available from the channel that consumes this custom patch. \ - Specify the location as a Maven GA coordinate (groupId:artifactId). \ No newline at end of file + Specify the location as a Maven GA coordinate (groupId:artifactId). + +# LIST COMPONENT CERTIFICATES +installer.certificates-list.description=\ + Lists all public keys imported into the server. The public keys are used to verify components installed during update and revert operation. + +# ADD COMPONENT CERTIFICATES +installer.certificates-add.description=\ + Imports a new public key to be used to verify components during update and revert operations. Once imported, as long as the key is not \ + expired or revoked, any artifact signed by it, will be treated as a trusted component. + +installer.certificates-add.option.cert-file.description=\ + Path to the file containing an ASCII-armored GPG public key to be imported. + +installer.certificates-add.option.non-interactive.description=\ + Switches off a user prompt asking to confirm the public key validity. Note, the user needs to be sure the file contains a valid key when using this option. + +# REMOVE COMPONENT CERTIFICATES +installer.certificates-remove.description=\ + Removes one of the public keys used to verify components during update and revert operations. + +installer.certificates-remove.option.key-id.description=\ + The ID in a hexadecimal form of a public key to be removed from the list of trusted component certificates. \ + Once the key is removed, if an update or revert operation tries to use component signed by this key, the user will be warned \ + and the update will be rejected unless the uses re-imports the key. \ No newline at end of file diff --git a/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java b/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java index 7e65046d51d..7963fe56d2b 100644 --- a/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java +++ b/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java @@ -11,12 +11,21 @@ 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.CERTIFICATE_CONTENT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FILE; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FINGERPRINT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_KEY_ID; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_STATUS; import java.io.File; import java.io.FileInputStream; @@ -65,7 +74,9 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.wildfly.installationmanager.ArtifactChange; import org.wildfly.installationmanager.Channel; import org.wildfly.installationmanager.ChannelChange; @@ -86,6 +97,9 @@ public class InstMgrResourceTestCase extends AbstractControllerTestBase { static final Path JBOSS_CONTROLLER_TEMP_DIR = JBOSS_HOME.resolve("temp"); static final Path INSTALLATION_MANAGER_PROPERTIES = JBOSS_HOME.resolve("bin").resolve("installation-manager.properties"); + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Before public void setupController() throws InterruptedException, IOException { TestInstallationManager.initialized = false; @@ -357,6 +371,111 @@ public void testRemoveChannels() throws Exception { ); } + @Test + public void testReadCertificates() throws Exception { + PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode op = Util.createEmptyOperation(READ_RESOURCE_OPERATION, pathElements); + op.get(INCLUDE_RUNTIME).set(true); + + ModelNode result = executeForResult(op); + Assert.assertTrue(result.isDefined()); + + // validate the result: + List certs = result.get(InstMgrConstants.CERTIFICATES).asListOrEmpty(); + Assert.assertEquals(1, certs.size()); + + // First Channel + ModelNode cert = certs.get(0); + Assert.assertEquals("abcd", cert.get(CERT_KEY_ID).asString()); + Assert.assertEquals("abcd1234", cert.get(CERT_FINGERPRINT).asString()); + Assert.assertEquals("Test Cert", cert.get(CERT_DESCRIPTION).asString()); + Assert.assertEquals("TRUSTED", cert.get(CERT_STATUS).asString()); + } + + @Test + public void testRemoveCertificate() throws Exception { + PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode op = Util.createEmptyOperation(InstMgrCertificateRemoveHandler.OPERATION_NAME, pathElements); + op.get(INCLUDE_RUNTIME).set(true); + op.get(CERT_KEY_ID).set("abcd"); + + executeForResult(op); + + // verify there are no certificates now + pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode listOp = Util.createEmptyOperation(READ_RESOURCE_OPERATION, pathElements); + listOp.get(INCLUDE_RUNTIME).set(true); + + ModelNode result = executeForResult(listOp); + Assert.assertTrue(result.isDefined()); + + // validate the result is an empty list: + List certs = result.get(InstMgrConstants.CERTIFICATES).asListOrEmpty(); + Assert.assertEquals(0, certs.size()); + } + + @Test + public void testAddCertificate() throws Exception { + PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode op = Util.createEmptyOperation(InstMgrCertificateImportHandler.OPERATION_NAME, pathElements); + op.get(INCLUDE_RUNTIME).set(true); + op.get(CERT_FILE).set(0); + final OperationBuilder builder = OperationBuilder.create(op); + final File file = temp.newFile("test.crt"); + Files.writeString(file.toPath(), "key-id:efgh\nfingerprint:efgh5678\ndescription:Another Cert"); + builder.addFileAsAttachment(file); + + executeForResult(builder.build()); + + // verify there are two certificates now + pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode listOp = Util.createEmptyOperation(READ_RESOURCE_OPERATION, pathElements); + listOp.get(INCLUDE_RUNTIME).set(true); + + ModelNode result = executeForResult(listOp); + Assert.assertTrue(result.isDefined()); + + // validate the result has two elements: + List certs = result.get(InstMgrConstants.CERTIFICATES).asListOrEmpty(); + Assert.assertEquals(2, certs.size()); + + // First Channel + ModelNode cert = certs.get(0); + Assert.assertEquals("abcd", cert.get(CERT_KEY_ID).asString()); + Assert.assertEquals("abcd1234", cert.get(CERT_FINGERPRINT).asString()); + Assert.assertEquals("Test Cert", cert.get(CERT_DESCRIPTION).asString()); + Assert.assertEquals("TRUSTED", cert.get(CERT_STATUS).asString()); + + // Second Channel + cert = certs.get(1); + Assert.assertEquals("efgh", cert.get(CERT_KEY_ID).asString()); + Assert.assertEquals("efgh5678", cert.get(CERT_FINGERPRINT).asString()); + Assert.assertEquals("Another Cert", cert.get(CERT_DESCRIPTION).asString()); + Assert.assertEquals("TRUSTED", cert.get(CERT_STATUS).asString()); + } + + @Test + public void testParseCertificate() throws Exception { + PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode op = Util.createEmptyOperation(InstMgrCertificateParseHandler.OPERATION_NAME, pathElements); + op.get(INCLUDE_RUNTIME).set(true); + op.get(CERT_FILE).set(0); + final OperationBuilder builder = OperationBuilder.create(op); + final File file = temp.newFile("test.crt"); + Files.writeString(file.toPath(), "key-id:efgh\nfingerprint:efgh5678\ndescription:Another Cert"); + builder.addFileAsAttachment(file); + + ModelNode result = executeForResult(builder.build()); + Assert.assertTrue(result.isDefined()); + + // validate the result is a new cert: + ModelNode cert = result.asObject(); + Assert.assertEquals("efgh", cert.get(CERT_KEY_ID).asString()); + Assert.assertEquals("efgh5678", cert.get(CERT_FINGERPRINT).asString()); + Assert.assertEquals("Another Cert", cert.get(CERT_DESCRIPTION).asString()); + Assert.assertEquals("TRUSTED", cert.get(CERT_STATUS).asString()); + } + @Test public void testHistoryChannels() throws Exception { PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); @@ -1430,6 +1549,23 @@ 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 = rsp.get(RESULT).asList().get(0).get(CERTIFICATE_CONTENT).asString(); + + Assert.assertEquals("key-id:abcd\n" + + "fingerprint:abcd1234\n" + + "description:Missing 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. diff --git a/pom.xml b/pom.xml index a4371fa2245..16cd5b30174 100644 --- a/pom.xml +++ b/pom.xml @@ -240,7 +240,7 @@ 1.0.1.Final 1.7.0.Final 1.3.0.Final - 1.0.3.Final + 2.0.0.Beta2-SNAPSHOT 8.0.2.Final 2.2.5.Final 2.2.2.Final diff --git a/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java b/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java index 91b5a3928e4..3226d076074 100644 --- a/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java +++ b/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java @@ -5,31 +5,41 @@ package org.wildfly.test.installationmanager; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; 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; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.wildfly.installationmanager.ArtifactChange; +import org.wildfly.installationmanager.CandidateType; import org.wildfly.installationmanager.Channel; import org.wildfly.installationmanager.ChannelChange; +import org.wildfly.installationmanager.FileConflict; import org.wildfly.installationmanager.HistoryResult; import org.wildfly.installationmanager.InstallationChanges; import org.wildfly.installationmanager.ManifestVersion; import org.wildfly.installationmanager.MavenOptions; import org.wildfly.installationmanager.OperationNotAvailableException; import org.wildfly.installationmanager.Repository; +import org.wildfly.installationmanager.TrustCertificate; import org.wildfly.installationmanager.spi.InstallationManager; import org.wildfly.installationmanager.spi.OsShell; @@ -44,6 +54,7 @@ public class TestInstallationManager implements InstallationManager { public static List findUpdatesRepositories; public static List findUpdatesChanges; public static List prepareUpdatesRepositories; + private static List lstTrustCertificates; public static Path prepareUpdatesTargetDir; public static List prepareRevertRepositories; @@ -119,10 +130,10 @@ public static void initialize() throws IOException { // History sample data history = new HashMap<>(); - history.put("update", new HistoryResult("update", Instant.now(), "update", "update description")); - history.put("install", new HistoryResult("install", Instant.now(), "install", "install description")); - history.put("rollback", new HistoryResult("rollback", Instant.now(), "rollback", "rollback description")); - history.put("config_change", new HistoryResult("config_change", Instant.now(), "config_change", "config_change description")); + history.put("update", new HistoryResult("update", Instant.now(), "update", "update description", Collections.emptyList())); + history.put("install", new HistoryResult("install", Instant.now(), "install", "install description", Collections.emptyList())); + history.put("rollback", new HistoryResult("rollback", Instant.now(), "rollback", "rollback description", Collections.emptyList())); + history.put("config_change", new HistoryResult("config_change", Instant.now(), "config_change", "config_change description", Collections.emptyList())); // List Updates sample Data @@ -157,6 +168,10 @@ public static void initialize() throws IOException { } } + // certificates sample data + lstTrustCertificates = new ArrayList<>(); + lstTrustCertificates.add(new TrustCertificate("abcd", "abcd1234", "Test Cert", "TRUSTED")); + initialized = true; } } @@ -266,11 +281,81 @@ public String generateApplyRevertCommand(Path scriptHome, Path candidatePath, Os return scriptHome + APPLY_REVERT_BASE_GENERATED_COMMAND + candidatePath.toString(); } + @Override + public String generateApplyUpdateCommand(Path scriptHome, Path candidatePath, OsShell shell, boolean noConflictsOnly) throws OperationNotAvailableException { + return scriptHome + APPLY_UPDATE_BASE_GENERATED_COMMAND + candidatePath.toString(); + } + + @Override + public String generateApplyRevertCommand(Path scriptHome, Path candidatePath, OsShell shell, boolean noConflictsOnly) throws OperationNotAvailableException { + return scriptHome + APPLY_REVERT_BASE_GENERATED_COMMAND + candidatePath.toString(); + } + @Override public Collection getInstalledVersions() throws Exception { return installedVersions; } + @Override + public Collection verifyCandidate(Path candidatePath, CandidateType candidateType) throws Exception { + return Collections.emptySet(); + } + + @Override + public void acceptTrustedCertificates(InputStream certificate) throws Exception { + final TrustCertificate trustCertificate = parseCertificate(certificate); + + lstTrustCertificates.add(trustCertificate); + } + + @Override + public void revokeTrustedCertificate(String keyID) throws Exception { + final Optional cert = lstTrustCertificates.stream() + .filter(c -> c.getKeyID().equals(keyID)) + .findFirst(); + + cert.ifPresent(trustCertificate -> lstTrustCertificates.remove(trustCertificate)); + } + + @Override + public Collection listTrustedCertificates() throws Exception { + return Collections.unmodifiableCollection(lstTrustCertificates); + } + + @Override + public TrustCertificate parseCertificate(InputStream certificate) throws Exception { + String keyId = null; + String fingerprint = null; + String description = null; + try(BufferedReader reader = new BufferedReader(new InputStreamReader(certificate))) { + while (reader.ready()) { + final String[] line = reader.readLine().split(":"); + switch (line[0]) { + case "key-id": + keyId = line[1]; + break; + case "fingerprint": + fingerprint = line[1]; + break; + case "description": + description = line[1]; + break; + default: + throw new RuntimeException("Unknown line: " + line[0]); + } + } + } + + return new TrustCertificate(keyId, fingerprint, description, "TRUSTED"); + } + + @Override + public Collection downloadRequiredCertificates() throws Exception { + final String cert = "key-id:abcd\nfingerprint:abcd1234\ndescription:Missing 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());