public CertificateBasedStack()

in cross-account-publish/certificate-based-stack/src/main/java/com/awssamples/crossaccountpublish/CertificateBasedStack.java [57:229]


    public CertificateBasedStack(final Construct parent, final String name) {
        super(parent, name);

        // Inject dependencies
        DaggerInjector.create().inject(this);

        Option<String> destroyArgument = getDestroyArgument();

        if (destroyArgument.isDefined()) {
            return;
        }

        Option<String> csrFileArgument = getCsrFileArgument();
        Option<String> desiredTopicArgument = getDesiredTopicArgument();

        if (csrFileArgument.isEmpty() && desiredTopicArgument.isEmpty()) {
            throw new RuntimeException("In the context variables either a CSR filename [" + CSR_FILE_VARIABLE + "] or a desired topic/topic hierarchy [" + DESIRED_TOPIC_VARIABLE + "] must be specified");
        }

        if (csrFileArgument.isDefined() && desiredTopicArgument.isDefined()) {
            throw new RuntimeException("In the context variables either a CSR filename [" + CSR_FILE_VARIABLE + "] or a desired topic/topic hierarchy [" + DESIRED_TOPIC_VARIABLE + "] must be specified, but not both");
        }

        // Build all of the necessary JARs
        projectDirectory = "./";
        outputArtifactName = String.join("-", name, "all.jar");

        build();

        // Make sure we look for the CSR file argument in the right directory
        csrFileArgument = csrFileArgument.map(csrFile -> String.join("", projectDirectory, csrFile));

        // Use the existing CSR or the default one
        File csrFile = new File(csrFileArgument.getOrElse(() -> String.join("", projectDirectory, String.join("-", FULL_KEY_PREFIX, "public.csr"))));

        if (csrFileArgument.isEmpty()) {
            // We are creating the CSR

            // Must have a desired topic
            String desiredTopic = desiredTopicArgument.get();

            // Create a new private key
            File temporaryCaPrivateKeyFile = new File(String.join("", projectDirectory, String.join("-", FULL_KEY_PREFIX, "private.pem")));
            log.info("Creating temporary CA private key file [" + temporaryCaPrivateKeyFile.getAbsolutePath() + "]");
            KeyPair keyPair = iotHelper.getRandomEcKeypair(384);
            Try.run(() -> writeFile(temporaryCaPrivateKeyFile.getAbsolutePath(), iotHelper.toPem(keyPair))).get();

            // Create CSR
            PKCS10CertificationRequest pkcs10CertificationRequest = iotHelper.generateCertificateSigningRequest(keyPair, List.of(Tuple.of("CN", desiredTopic)));
            Try.run(() -> writeFile(csrFile.getAbsolutePath(), iotHelper.toPem(pkcs10CertificationRequest))).get();
        }

        // At this point we have the provided CSR or our generated CSR

        // Convert from PEM back to CSR and CSR back to PEM just to be sure that the data is valid in case it was edited or corrupted between runs
        Try<PKCS10CertificationRequest> csrTry = iotHelper.tryGetObjectFromPem(csrFile, PKCS10CertificationRequest.class);

        // Get the CSR string so we can add it to the Lambda backed custom resource's environment
        String csrString = csrTry.map(iotHelper::toPem)
                .getOrElseThrow(() -> new RuntimeException("Couldn't read CSR file or the CSR file is invalid [" + csrFile.getAbsolutePath() + "]"));

        // Extract the common name which is the topic/topic hierarchy they would like to publish on
        String allowedTopic = csrTry.toStream()
                // Get the X500Name (subject)
                .map(PKCS10CertificationRequest::getSubject)
                // Get all of the relative distinguished names (RDNs)
                .map(X500Name::getRDNs)
                // Turn all of the RDNs into a single stream
                .flatMap(Stream::of)
                // Get all of the attribute types and values
                .map(RDN::getTypesAndValues)
                // Turn all of the attribute types and values into a single stream
                .flatMap(Stream::of)
                // Find the common name (CN) attribute
                .filter(value -> value.getType().equals(BCStyle.CN))
                // Extract the CN value
                .map(AttributeTypeAndValue::getValue)
                // Convert the CN value to a string
                .map(Object::toString)
                // Undo the URL style encoding (convert %2F to forward slash)
                .map(value -> value.replace("%2F", "/"))
                // If no common name was found in the CSR then throw an exception
                .getOrElseThrow(() -> new RuntimeException("No common name value was found in the CSR"));

        // Allow this certificate to publish only on the provided topic
        List<Statement> statement = List.of(
                Statement.allowIamAction(IotActions.publish(IotResources.topic(allowedTopic))));

        PolicyDocument policyDocument = new PolicyDocument();
        policyDocument.Statement = statement.asJava();

        String policyName = Fn.join("-", List.of(this.getStackName(), PARTNER_POLICY).asJava());

        CfnPolicyProps cfnPolicyProps = CfnPolicyProps.builder()
                .policyDocument(policyDocument)
                .policyName(policyName)
                .build();

        CfnPolicy cfnPolicy = new CfnPolicy(this, PARTNER_POLICY, cfnPolicyProps);

        // Add the CSR to the custom resource request
        CustomResourceProps.Builder customResourcePropsBuilder = CustomResourceProps.builder()
                .properties(
                        HashMap.of("CSR", csrString,
                                        "AllowedTopic", allowedTopic)
                                .toJavaMap());

        // Build a publish policy for the certificate
        List<CustomResource> customResourceList = CustomResourceHelper.getCustomResources(this, getOutputArtifactFile(), Option.of(customResourcePropsBuilder), Option.of(functionPropsBuilder));

        if (customResourceList.size() != 1) {
            throw new RuntimeException("This stack only expects that one custom resource is present but it found [" + customResourceList.size() + "]");
        }

        CustomResource customResource = customResourceList.get();
        String certificatePem = customResource.getAtt("pem").toString();
        String certificateFingerprint = customResource.getAtt("fingerprint").toString();

        CfnCertificateProps cfnCertificateProps = CfnCertificateProps.builder()
                .certificatePem(certificatePem)
                // Set the certificate as inactive so the customer can validate that everything is set up as they expected before granting access
                .status("INACTIVE")
                .certificateMode("SNI_ONLY")
                .build();

        CfnCertificate cfnCertificate = new CfnCertificate(this, "Certificate", cfnCertificateProps);

        String certificateArn = Fn.getAtt(cfnCertificate.getLogicalId(), "Arn").toString();

        // URL to link directly to the certificate
        new CfnOutput(this, "CertificateURL", CfnOutputProps.builder()
                .exportName("CertificateURL")
                .value(Fn.join("", List.of("https://console.aws.amazon.com/iot/home?region=", Aws.REGION, "#/certificate/", certificateFingerprint).asJava()))
                .build());

        // The certificate ARN
        new CfnOutput(this, "CertificateARN", CfnOutputProps.builder()
                .exportName("CertificateARN")
                .value(certificateArn)
                .build());

        new CfnOutput(this, "CertificateID", CfnOutputProps.builder()
                .exportName("CertificateID")
                .value(certificateFingerprint)
                .build());

        String certificateFileName = Fn.join(".", List.of(certificateFingerprint, "pem").asJava());

        new CfnOutput(this, "CertificatePEMFile", CfnOutputProps.builder()
                .exportName("CertificatePEMFile")
                .value(certificateFileName)
                .build());

        // The command to get the certificate PEM into a file
        new CfnOutput(this, "CertificatePEMCommand", CfnOutputProps.builder()
                .exportName("CertificatePEMCommand")
                .value(Fn.join("", List.of("aws iot describe-certificate --certificate-id ", certificateFingerprint, " --query certificateDescription.certificatePem --output text > ", certificateFingerprint, ".pem").asJava()))
                .build());

        new CfnOutput(this, "CertificateActivateCommand", CfnOutputProps.builder()
                .exportName("CertificateActivateCommand")
                .value(Fn.join("", List.of("aws iot update-certificate --certificate-id ", certificateFingerprint, " --new-status ACTIVE").asJava()))
                .build());

        CfnPolicyPrincipalAttachmentProps cfnPolicyPrincipalAttachmentProps = CfnPolicyPrincipalAttachmentProps.builder()
                .policyName(cfnPolicy.getPolicyName())
                .principal(certificateArn)
                .build();

        new CfnPolicyPrincipalAttachment(this, "PrincipalPolicyAttachment", cfnPolicyPrincipalAttachmentProps)
                // Depends on the policy, must delete the attachment first
                .addDependsOn(cfnPolicy);
    }