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);
}