Workflow

The certificate signing request and certificate revocation request workflow can be extended by custom workflow plugin. This can be used to synchronise statuses and interact between the CENM workflow and external workflow/ticketing system like JIRA.

Adding workflow plugin

The workflow plugin can be configured via configuration file.

For certificate signing request:

workflows {
    issuer {
        type = ISSUANCE
        updateInterval = 10000

        enmListener = {
            port = 6000
        }

        plugin {
            pluginClass = "com.r3.enm.services.identitymanager.workflow.StubbedWorkflowPlugin"
            config {
                customConfig = "some config"
                .
                .
                .
            }
        }
    }
}

For certificate revocation request:

workflows {
    issuer {
        type = ISSUANCE
        updateInterval = 10000

        enmListener = {
            port = 6000
        }

        plugin {
            pluginClass = "com.r3.enm.services.identitymanager.workflow.StubbedWorkflowPlugin"
            config {
                customConfig = "some config"
                .
                .
                .
            }
        }
    },
    revoker {
        type = REVOCATION
        crlCacheTimeout = 1000
        crlFiles = ["./crl-files/subordinate.crl"]
        enmListener = {
            port = 6001
            reconnect = true
            ssl = {
                keyStore = {
                    location = ./certificates/corda-ssl-identity-manager-keys.jks
                    password = password
                }
                trustStore = {
                    location = ./certificates/corda-ssl-trust-store.jks
                    password = trustpass
                }
            }
        }

        plugin = {
            pluginClass = "com.r3.enm.services.identitymanager.workflow.StubbedWorkflowPlugin"
            config {
                customConfig = "some config"
                .
                .
                .
            }
        }
    }
}

Creating a workflow plugin

The workflow plugin must extend WorkflowPlugin for certificate signing request or certificate revocation request respectively, issuance and revocation workflows can be configured with specific plugin classes as per the configuration shown above. The plugin will need to be made available to the CENM process by including the plugin .jar in the classpath. This can be done by specifying the .jar path via the pluginJar configuration option.

/*
 * R3 Proprietary and Confidential
 *
 * Copyright (c) 2018 R3 Limited.  All rights reserved.
 *
 * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
 *
 * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
 */

package com.r3.enm.workflow.api

import com.r3.enm.model.Request
import com.r3.enm.nsdefaults.ENMDefaults
import net.corda.core.identity.CordaX500Name

/**
 * Workflow plugin interface for adding custom functionality to the network services workflow.
 * @param <R> Type of the request
 */
interface WorkflowPlugin<R : Request> {
    /**
     * Create ticket in the external system. The method will ensure that the ticket is created.
     * The request remains in [RequestStatus.NEW] state and a workflow may keep trying to create the ticket
     * in the external system until the request is updated to [RequestStatus.APPROVED],
     * [RequestStatus.REJECTED] or [RequestStatus.DONE].
     */
    fun createTicket(request: R)

    /**
     * Move ticket status to done state, the status of the request will be updated to [RequestStatus.DONE] once this method is executed successfully.
     */
    fun markAsDone(request: R)

    /**
     * Retrieve ticket with the given request ID.
     */
    fun getRequest(requestId: String): WorkflowPluginRequest?

    /**
     * Runs the interactive shell commands of the plugin.
     */
    fun runCommandDriver() {}

    /**
     * Retrieve the alias for the plugin.
     */
    fun getAlias(): String = ENMDefaults.NOT_AVAILABLE
}

/**
 * Command driver interface for adding custom functionality for interacting with users via the interactive shell
 */
interface CommandDriver {
/**
 * Outputs a menu based on the available commands defined within the class, containing the methods for handling any
 * further interaction logic.
 */
    fun showMenu()
}

/**
 * A class representing the workflow plugin request data.
 */
data class WorkflowPluginRequest(val requestId: String,
                                 val status: RequestStatus,
                                 val modifiedBy: String? = null,
                                 val rejectionData: RejectionData? = null,
                                 val legalName: CordaX500Name? = null)

Example 1

This sample workflow plugin creates a request file in basedir when the Identity Manager received a certificate signing request, user can then approve or reject the request by moving the request files to approved or rejected folder. The certificate signing process will then issue a certificate for the request (require signer configuration), and move the request files to done folder.

Config file:

address = "localhost:1300"

workflows {
    issuer {
        type = ISSUANCE
        updateInterval = 10000

        enmListener = {
            port = 6000
        }

        plugin {
            pluginClass = "com.r3.enmplugins.example.FileBaseCSRPlugin"
            config {
                baseDir = "workflowDirectory"
            }
        }
    }
}

File base plugin implementation:

package com.r3.enmplugins.example

import com.r3.enm.model.CertificateSigningRequest
import com.r3.enm.model.RejectionReason
import com.r3.enm.workflow.api.RejectionData
import com.r3.enm.workflow.api.RequestStatus
import com.r3.enm.workflow.api.WorkflowPlugin
import com.r3.enm.workflow.api.WorkflowPluginRequest
import com.r3.enm.workflow.api.plugins.PluginLogger
import com.typesafe.config.Config
import net.corda.core.internal.createDirectories
import net.corda.core.internal.list
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.FileOwnerAttributeView

class FileBaseCSRPlugin(
        config: Config?,
        val logger: PluginLogger
) : WorkflowPlugin<CertificateSigningRequest> {
    private val baseDir = if (config != null) {
        Paths.get(config.getString("baseDir") ?: "workflowDirectory")
    } else {
        Paths.get("workflowDirectory")
    }.also { it.createDirectories() }
    private val approvedFolder = baseDir.resolve("approved").also { it.createDirectories() }
    private val rejectedFolder = baseDir.resolve("rejected").also { it.createDirectories() }
    private val doneFolder = baseDir.resolve("done").also { it.createDirectories() }

    override fun getRequest(requestId: String): WorkflowPluginRequest? {
        logger.info("Retrieving CSR with id $requestId")
        return findWorkflowPluginRequest(requestId, baseDir, RequestStatus.NEW)
                ?: findWorkflowPluginRequest(requestId, approvedFolder, RequestStatus.APPROVED)
                ?: findWorkflowPluginRequest(requestId, rejectedFolder, RequestStatus.REJECTED)
                ?: findWorkflowPluginRequest(requestId, doneFolder, RequestStatus.DONE)
    }

    private fun findWorkflowPluginRequest(requestId: String, directory: Path, status: RequestStatus): WorkflowPluginRequest? {
        return directory.list().findLast { it.toFile().name == requestId }?.let {
            WorkflowPluginRequest(
                    requestId,
                    status,
                    Files.getFileAttributeView(it, FileOwnerAttributeView::class.java).owner.name,
                    RejectionData(RejectionReason.UNKNOWN, "Not specified"))
        }
    }

    override fun createTicket(request: CertificateSigningRequest) {
        val subject = request.legalName

        val data = mapOf(
                "Common Name" to subject.commonName,
                "Organisation" to subject.organisation,
                "Organisation Unit" to subject.organisationUnit,
                "State" to subject.state,
                "Nearest City" to subject.locality,
                "Country" to subject.country,
                "X500 Name" to subject.toString())

        val requestPemString = StringWriter().apply {
            JcaPEMWriter(this).use {
                it.writeObject(PemObject("CERTIFICATE REQUEST", request.pkcS10CertificationRequest.encoded))
            }
        }.toString()

        val ticketDescription = data.filter { it.value != null }.map { "${it.key}: ${it.value}" } + requestPemString

        Files.write(baseDir.resolve(request.requestId), ticketDescription)

        logger.info("Creating Certificate Signing Request ticket for request : ${request.requestId}")
    }

    override fun markAsDone(request: CertificateSigningRequest) {
        logger.info("Marking CSR as done")
        Files.move(approvedFolder.resolve(request.requestId), doneFolder.resolve(request.requestId))
    }
}

Example 2

This sample workflow auto-approves CSRs based on a token provided in the request.

Config file:

address = "localhost:1300"

workflows {
    issuer {
        type = ISSUANCE
        updateInterval = 10000

        enmListener = {
            port = 6000
        }

        plugin {
            pluginClass = "com.r3.enm.csrplugin.ExamplePlugin"
            pluginJar = "workflowPluginExample.jar"
            config {
                .
                .
                .
            }
        }
    }
}

Auto-approval plugin implementation:

class ExamplePlugin(config: Config?, val logger: PluginLogger) : WorkflowPlugin<CertificateSigningRequest> {

    private val done = ConcurrentHashMap<String, WorkflowPluginRequest?>()
    private val approved = ConcurrentHashMap<String, WorkflowPluginRequest?>()
    private val rejected = ConcurrentHashMap<String, WorkflowPluginRequest?>()

    override fun createTicket(request: CertificateSigningRequest) {
        logger.info {"Creating CSR with id ${request.requestId}"}
        if (validate(request.submissionToken)) {
            approved[request.requestId] = WorkflowPluginRequest(request.requestId, RequestStatus.APPROVED, "Me")
        } else {
            rejected[request.requestId] = WorkflowPluginRequest(request.requestId, RequestStatus.REJECTED, "Me", RejectionData(
                RejectionReason.UNPARSEABLE("Missing or unknown CSR token"), "Git gud!")
            )
        }
    }

    override fun getRequest(requestId: String): WorkflowPluginRequest? {
        logger.info { "Fetching request $requestId" }
        return done.getOrDefault(requestId, approved.getOrDefault(requestId, rejected.getOrDefault(requestId, null)))
    }

    override fun markAsDone(request: CertificateSigningRequest) {
        logger.info { "Marking CSR ${request.requestId} as done" }
        with (request.requestId) {
            if (approved.containsKey(this)) {
                done[this] = approved[this]?.copy(status = RequestStatus.DONE)?.also { approved.remove(this) }
            } else if (rejected.contains(this)) {
                done[this] = rejected[this]?.copy(status = RequestStatus.DONE)?.also { rejected.remove(this) }
            }
        }
    }

    private fun validate(token: String?): Boolean {
        return token?.let {
            it == "CENM"
        } ?: false
    }
}

Certificate Signing Request Rejection Reasons

The workflow is expected to provide a valid rejection reason (see below for allowed values) for each certificate signing request being rejected. Those rejection reasons are then forwarded and passed to a node in its certificate signing request polling status check response. Specifically, such response will have the rejection reason code set in its response “CSR-Rejection-Reason” header as well as in the response body, which additionally is extended with the natural-language description of the rejection reason.

Permitted certificate signing request rejection reasons are as follows:

Rejection Code Type Rejection Description
0 UNKNOWN The rejection reason is not known.
1 INCORRECT_X500_NAME_FORMAT Legal name is incorrectly formatted.
2 DUPLICATE_X500_NAME Legal name is duplicated.
3 DUPLICATE_PUBLIC_KEY Public key is duplicated.
4 CERTIFICATE_ROLE_NOT_ALLOWED Requested certificate role is not allowed.
5 REQUESTED_BY_NODE_OPERATOR Rejection requested by the node operator.
6 REQUESTED_BY_NETWORK_OPERATOR Rejection requested by the business network operator.
7 CHECK_NOT_PASSED Legal entity check not passed.
8 SANCTIONED On a Sanctions Watchlist.
9 EMAIL_DOES_NOT_MATCH Domain of email address listed in X500 does not match Legal Entity owner (according to whois.com).
10 PTOU_NOT_SIGNED Participant Terms Of Use not signed.
11 OTHER Other.

Node CSR Rejection Response

Node CSR rejection response follows the following format:

“Rejection reason code: «Rejection Code». Rejection reason description: «Rejection Description». Additional remark: «Remark If Any».”

Example response to a node when its CSR has been rejected:

HTTP Response header:

“CSR-Rejection-Reason”: [“8”]

Response Body:

Rejection reason code: 8. Description: On a Sanctions Watchlist. Additional remark: No additional remark.”

Was this page helpful?

Thanks for your feedback!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.

We're sorry this page wasn't helpful. Let us know how we can make it better!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Create an issue

Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.