diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilder.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilder.java index 016b2b36..fe15dc60 100644 --- a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilder.java +++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilder.java @@ -32,6 +32,7 @@ import java.util.List; import edu.umd.cs.findbugs.annotations.Nullable; import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; @@ -144,6 +145,49 @@ public class SMIMECSRBuilder { Arrays.stream(ids).forEach(this::addIdentifier); } + /** + * Sets an entry of the subject used for the CSR. + *
+ * This method is meant as "expert mode" for setting attributes that are not covered + * by the other methods. It is at the discretion of the ACME server to accept this + * parameter. + * + * @param attName + * The BCStyle attribute name + * @param value + * The value + * @throws AddressException + * if a common name is added, but the value is not a valid email address. + * @since 2.14 + */ + public void addValue(String attName, String value) throws AddressException { + ASN1ObjectIdentifier oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, "attribute name must not be null")); + addValue(oid, value); + } + + /** + * Sets an entry of the subject used for the CSR + *
+ * This method is meant as "expert mode" for setting attributes that are not covered + * by the other methods. It is at the discretion of the ACME server to accept this + * parameter. + * + * @param oid + * The OID of the attribute to be added + * @param value + * The value + * @throws AddressException + * if a common name is added, but the value is not a valid email address. + * @since 2.14 + */ + public void addValue(ASN1ObjectIdentifier oid, String value) throws AddressException { + if (requireNonNull(oid, "OID must not be null").equals(BCStyle.CN)) { + addEmail(new InternetAddress(value)); + return; + } + namebuilder.addRDN(oid, requireNonNull(value, "attribute value must not be null")); + } + /** * Sets the organization. *
diff --git a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilderTest.java b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilderTest.java index d02f6d4b..45b7603e 100644 --- a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilderTest.java +++ b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/csr/SMIMECSRBuilderTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.smime.csr; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayOutputStream; @@ -29,6 +30,7 @@ import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERBitString; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.pkcs.Attribute; @@ -144,6 +146,58 @@ public class SMIMECSRBuilderTest { keyUsageTest(csr, KeyUsage.digitalSignature | KeyUsage.keyEncipherment); } + /** + * Checks that addValue behaves correctly in dependence of the attributes being added. + * If a common name is set, it should be handled in the same way when it's added by + * using {@link SMIMECSRBuilder#addEmail(InternetAddress)}. + */ + @Test + public void testAddAttrValues() throws Exception { + SMIMECSRBuilder builder = new SMIMECSRBuilder(); + String invAttNameExMessage = assertThrows(IllegalArgumentException.class, + () -> X500Name.getDefaultStyle().attrNameToOID("UNKNOWNATT")).getMessage(); + + assertThat(builder.toString()).isEqualTo(",TYPE=SIGNING_AND_ENCRYPTION"); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new SMIMECSRBuilder().addValue((String) null, "value")) + .as("addValue(String, String)"); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new SMIMECSRBuilder().addValue((ASN1ObjectIdentifier) null, "value")) + .as("addValue(ASN1ObjectIdentifier, String)"); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> new SMIMECSRBuilder().addValue("C", null)) + .as("addValue(String, null)"); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new SMIMECSRBuilder().addValue("UNKNOWNATT", "val")) + .as("addValue(String, null)") + .withMessage(invAttNameExMessage); + assertThatExceptionOfType(AddressException.class) + .isThrownBy(() -> new SMIMECSRBuilder().addValue("CN", "invalid@example..com")) + .as("addValue(String, invalid String)"); + + assertThat(builder.toString()).isEqualTo(",TYPE=SIGNING_AND_ENCRYPTION"); + + builder.addValue("C", "DE"); + assertThat(builder.toString()).isEqualTo("C=DE,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue("E", "contact@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue("CN", "firstcn@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue("CN", "scnd@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,EMAIL=scnd@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + + builder = new SMIMECSRBuilder(); + builder.addValue(BCStyle.C, "DE"); + assertThat(builder.toString()).isEqualTo("C=DE,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue(BCStyle.EmailAddress, "contact@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue(BCStyle.CN, "firstcn@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + builder.addValue(BCStyle.CN, "scnd@example.com"); + assertThat(builder.toString()).isEqualTo("C=DE,E=contact@example.com,CN=firstcn@example.com,EMAIL=firstcn@example.com,EMAIL=scnd@example.com,TYPE=SIGNING_AND_ENCRYPTION"); + } + /** * Checks if the S/MIME CSR contains the right parameters. *
diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java index 99b58a7c..23ce7c37 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java @@ -33,7 +33,6 @@ import java.util.List; import java.util.Objects; import edu.umd.cs.findbugs.annotations.Nullable; - import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; @@ -185,13 +184,18 @@ public class CSRBuilder { public void addIdentifiers(Identifier... ids) { Arrays.stream(ids).forEach(this::addIdentifier); } - + /** - * Sets an entry of the subject used for the CSR + * Sets an entry of the subject used for the CSR. *
- * Note that it is at the discretion of the ACME server to accept this parameter. - * @param attName The BCStyle attribute name - * @param value The value + * This method is meant as "expert mode" for setting attributes that are not covered + * by the other methods. It is at the discretion of the ACME server to accept this + * parameter. + * + * @param attName + * The BCStyle attribute name + * @param value + * The value * @since 2.14 */ public void addValue(String attName, String value) { @@ -200,11 +204,16 @@ public class CSRBuilder { } /** - * Sets an entry of the subject used for the CSR + * Sets an entry of the subject used for the CSR. *
- * Note that it is at the discretion of the ACME server to accept this parameter. - * @param oid The OID of the attribute to be added - * @param value The value + * This method is meant as "expert mode" for setting attributes that are not covered + * by the other methods. It is at the discretion of the ACME server to accept this + * parameter. + * + * @param oid + * The OID of the attribute to be added + * @param value + * The value * @since 2.14 */ public void addValue(ASN1ObjectIdentifier oid, String value) {