aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-11-19 16:37:32 +0100
committerBjørn Christian Seime <bjorncs@oath.com>2018-11-19 16:37:35 +0100
commit64ad086cb1789f1881ff44d1432c905fecc07e41 (patch)
tree6e511dffa8039e3749303053d8fa3be7c4bd882b
parentaa8dfde70a0c045629d645f49c50a2963f8ff66a (diff)
Rewrite JSON serialization of TransportSecurityOptions
- Use Jackson data bindings on TransportSecurityOptionsEntity - Add serialization to JSON - Add AuthorizedPeers to TransportSecurityOptions
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java101
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java38
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java143
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java10
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java46
5 files changed, 301 insertions, 37 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java
index 67466179634..8770c69832e 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java
@@ -1,13 +1,18 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.security.tls;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.security.tls.json.TransportSecurityOptionsJsonSerializer;
+import com.yahoo.security.tls.policy.AuthorizedPeers;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
@@ -18,61 +23,89 @@ import java.util.Optional;
*/
public class TransportSecurityOptions {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Path privateKeyFile;
private final Path certificatesFile;
private final Path caCertificatesFile;
+ private final AuthorizedPeers authorizedPeers;
- public TransportSecurityOptions(String privateKeyFile, String certificatesFile, String caCertificatesFile) {
- this(Paths.get(privateKeyFile), Paths.get(certificatesFile), Paths.get(caCertificatesFile));
+ private TransportSecurityOptions(Builder builder) {
+ this.privateKeyFile = builder.privateKeyFile;
+ this.certificatesFile = builder.certificatesFile;
+ this.caCertificatesFile = builder.caCertificatesFile;
+ this.authorizedPeers = builder.authorizedPeers;
}
- public TransportSecurityOptions(Path privateKeyFile, Path certificatesFile, Path caCertificatesFile) {
- this.privateKeyFile = privateKeyFile;
- this.certificatesFile = certificatesFile;
- this.caCertificatesFile = caCertificatesFile;
+ public Optional<Path> getPrivateKeyFile() {
+ return Optional.of(privateKeyFile);
}
- public Path getPrivateKeyFile() {
- return privateKeyFile;
+ public Optional<Path> getCertificatesFile() {
+ return Optional.of(certificatesFile);
}
- public Path getCertificatesFile() {
- return certificatesFile;
+ public Optional<Path> getCaCertificatesFile() {
+ return Optional.of(caCertificatesFile);
}
- public Path getCaCertificatesFile() {
- return caCertificatesFile;
+ public Optional<AuthorizedPeers> getAuthorizedPeers() {
+ return Optional.of(authorizedPeers);
}
public static TransportSecurityOptions fromJsonFile(Path file) {
- try {
- return fromJsonNode(mapper.readTree(file.toFile()));
+ try (InputStream in = Files.newInputStream(file)) {
+ return new TransportSecurityOptionsJsonSerializer().deserialize(in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static TransportSecurityOptions fromJson(String json) {
- try {
- return fromJsonNode(mapper.readTree(json));
+ return new TransportSecurityOptionsJsonSerializer()
+ .deserialize(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ public String toJson() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new TransportSecurityOptionsJsonSerializer().serialize(out, this);
+ return new String(out.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ public void toJsonFile(Path caCertificatesFile) {
+ try (OutputStream out = Files.newOutputStream(caCertificatesFile)) {
+ new TransportSecurityOptionsJsonSerializer().serialize(out, this);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
}
- private static TransportSecurityOptions fromJsonNode(JsonNode root) {
- JsonNode filesNode = getField(root, "files");
- String privateKeyFile = getField(filesNode, "private-key").asText();
- String certificatesFile = getField(filesNode, "certificates").asText();
- String caCertificatesFile = getField(filesNode, "ca-certificates").asText();
- return new TransportSecurityOptions(privateKeyFile, certificatesFile, caCertificatesFile);
- }
+ public static class Builder {
+ private Path privateKeyFile;
+ private Path certificatesFile;
+ private Path caCertificatesFile;
+ private AuthorizedPeers authorizedPeers;
+
+ public Builder() {}
- private static JsonNode getField(JsonNode root, String fieldName) {
- return Optional.ofNullable(root.get(fieldName))
- .orElseThrow(() -> new IllegalArgumentException(String.format("'%s' field missing", fieldName)));
+ public Builder withCertificate(Path certificatesFile, Path privateKeyFile) {
+ this.certificatesFile = certificatesFile;
+ this.privateKeyFile = privateKeyFile;
+ return this;
+ }
+
+ public Builder withCaCertificate(Path caCertificatesFile) {
+ this.caCertificatesFile = caCertificatesFile;
+ return this;
+ }
+
+ public Builder withAuthorizedPeers(AuthorizedPeers authorizedPeers) {
+ this.authorizedPeers = authorizedPeers;
+ return this;
+ }
+
+ public TransportSecurityOptions build() {
+ return new TransportSecurityOptions(this);
+ }
}
@Override
@@ -81,6 +114,7 @@ public class TransportSecurityOptions {
"privateKeyFile=" + privateKeyFile +
", certificatesFile=" + certificatesFile +
", caCertificatesFile=" + caCertificatesFile +
+ ", authorizedPeers=" + authorizedPeers +
'}';
}
@@ -91,11 +125,12 @@ public class TransportSecurityOptions {
TransportSecurityOptions that = (TransportSecurityOptions) o;
return Objects.equals(privateKeyFile, that.privateKeyFile) &&
Objects.equals(certificatesFile, that.certificatesFile) &&
- Objects.equals(caCertificatesFile, that.caCertificatesFile);
+ Objects.equals(caCertificatesFile, that.caCertificatesFile) &&
+ Objects.equals(authorizedPeers, that.authorizedPeers);
}
@Override
public int hashCode() {
- return Objects.hash(privateKeyFile, certificatesFile, caCertificatesFile);
+ return Objects.hash(privateKeyFile, certificatesFile, caCertificatesFile, authorizedPeers);
}
} \ No newline at end of file
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java
new file mode 100644
index 00000000000..d1c2960243b
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Jackson bindings for transport security options
+ *
+ * @author bjorncs
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+class TransportSecurityOptionsEntity {
+
+ @JsonProperty("files") Files files;
+ @JsonProperty("allowed-peers") List<AuthorizedPeer> authorizedPeers = new ArrayList<>();
+
+ static class Files {
+ @JsonProperty("private-key") String privateKeyFile;
+ @JsonProperty("certificates") String certificatesFile;
+ @JsonProperty("ca-certificates") String caCertificatesFile;
+ }
+
+ static class AuthorizedPeer {
+ @JsonProperty("required-credentials") List<RequiredCredential> requiredCredentials = new ArrayList<>();
+ @JsonProperty("name") String name;
+ }
+
+ static class RequiredCredential {
+ @JsonProperty("field") CredentialField field;
+ @JsonProperty("must-match") String matchExpression;
+ }
+
+ enum CredentialField { CN, SAN_DNS }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java
new file mode 100644
index 00000000000..76019a9df4e
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java
@@ -0,0 +1,143 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls.json;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.security.tls.TransportSecurityOptions;
+import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.AuthorizedPeer;
+import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.CredentialField;
+import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.Files;
+import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.RequiredCredential;
+import com.yahoo.security.tls.policy.AuthorizedPeers;
+import com.yahoo.security.tls.policy.HostGlobPattern;
+import com.yahoo.security.tls.policy.PeerPolicy;
+import com.yahoo.security.tls.policy.RequiredPeerCredential;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * @author bjorncs
+ */
+public class TransportSecurityOptionsJsonSerializer {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ public TransportSecurityOptions deserialize(InputStream in) {
+ try {
+ TransportSecurityOptionsEntity entity = mapper.readValue(in, TransportSecurityOptionsEntity.class);
+ return toTransportSecurityOptions(entity);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public void serialize(OutputStream out, TransportSecurityOptions options) {
+ try {
+ mapper.writeValue(out, toTransportSecurityOptionsEntity(options));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static TransportSecurityOptions toTransportSecurityOptions(TransportSecurityOptionsEntity entity) {
+ TransportSecurityOptions.Builder builder = new TransportSecurityOptions.Builder();
+ Files files = entity.files;
+ if (files != null) {
+ if (files.certificatesFile != null && files.privateKeyFile != null) {
+ builder.withCertificate(Paths.get(files.certificatesFile), Paths.get(files.privateKeyFile));
+ } else if (files.certificatesFile != null || files.privateKeyFile != null) {
+ throw new IllegalArgumentException("Both 'private-key' and 'certificates' must be configured together");
+ }
+ if (files.caCertificatesFile != null) {
+ builder.withCaCertificate(Paths.get(files.caCertificatesFile));
+ }
+ }
+ List<AuthorizedPeer> authorizedPeersEntity = entity.authorizedPeers;
+ if (authorizedPeersEntity.size() > 0) {
+ builder.withAuthorizedPeers(new AuthorizedPeers(toPeerPolicies(authorizedPeersEntity)));
+ }
+ return builder.build();
+ }
+
+ private static Set<PeerPolicy> toPeerPolicies(List<AuthorizedPeer> authorizedPeersEntity) {
+ return authorizedPeersEntity.stream()
+ .map(TransportSecurityOptionsJsonSerializer::toPeerPolicy)
+ .collect(toSet());
+ }
+
+ private static PeerPolicy toPeerPolicy(AuthorizedPeer authorizedPeer) {
+ if (authorizedPeer.name == null) {
+ throw missingFieldException("name");
+ }
+ if (authorizedPeer.requiredCredentials.isEmpty()) {
+ throw missingFieldException("required-credentials");
+ }
+ return new PeerPolicy(authorizedPeer.name, toRequestPeerCredentials(authorizedPeer.requiredCredentials));
+ }
+
+ private static List<RequiredPeerCredential> toRequestPeerCredentials(List<RequiredCredential> requiredCredentials) {
+ return requiredCredentials.stream()
+ .map(TransportSecurityOptionsJsonSerializer::toRequiredPeerCredential)
+ .collect(toList());
+ }
+
+ private static RequiredPeerCredential toRequiredPeerCredential(RequiredCredential requiredCredential) {
+ if (requiredCredential.field == null) {
+ throw new IllegalArgumentException("field");
+ }
+ if (requiredCredential.matchExpression == null) {
+ throw new IllegalArgumentException("must-match");
+ }
+ return new RequiredPeerCredential(toField(requiredCredential.field), new HostGlobPattern(requiredCredential.matchExpression));
+ }
+
+ private static RequiredPeerCredential.Field toField(CredentialField field) {
+ switch (field) {
+ case CN: return RequiredPeerCredential.Field.CN;
+ case SAN_DNS: return RequiredPeerCredential.Field.SAN_DNS;
+ default: throw new IllegalArgumentException("Invalid field type: " + field);
+ }
+ }
+
+ private static TransportSecurityOptionsEntity toTransportSecurityOptionsEntity(TransportSecurityOptions options) {
+ TransportSecurityOptionsEntity entity = new TransportSecurityOptionsEntity();
+ entity.files = new Files();
+ options.getCaCertificatesFile().ifPresent(value -> entity.files.caCertificatesFile = value.toString());
+ options.getCertificatesFile().ifPresent(value -> entity.files.certificatesFile = value.toString());
+ options.getPrivateKeyFile().ifPresent(value -> entity.files.privateKeyFile = value.toString());
+ options.getAuthorizedPeers().ifPresent( authorizedPeers -> {
+ for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) {
+ AuthorizedPeer authorizedPeer = new AuthorizedPeer();
+ authorizedPeer.name = peerPolicy.peerName();
+ for (RequiredPeerCredential requiredPeerCredential : peerPolicy.requiredCredentials()) {
+ RequiredCredential requiredCredential = new RequiredCredential();
+ requiredCredential.field = toField(requiredPeerCredential.field());
+ requiredCredential.matchExpression = requiredPeerCredential.pattern().asString();
+ authorizedPeer.requiredCredentials.add(requiredCredential);
+ }
+ entity.authorizedPeers.add(authorizedPeer);
+ }
+ });
+ return entity;
+ }
+
+ private static CredentialField toField(RequiredPeerCredential.Field field) {
+ switch (field) {
+ case CN: return CredentialField.CN;
+ case SAN_DNS: return CredentialField.SAN_DNS;
+ default: throw new IllegalArgumentException("Invalid field type: " + field);
+ }
+ }
+
+ private static IllegalArgumentException missingFieldException(String fieldName) {
+ return new IllegalArgumentException(String.format("'%s' missing", fieldName));
+ }
+}
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java
index 84f71cf8fc2..d1299a3b777 100644
--- a/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java
+++ b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java
@@ -17,20 +17,22 @@ import static org.junit.Assert.*;
public class TransportSecurityOptionsTest {
private static final Path TEST_CONFIG_FILE = Paths.get("src/test/resources/transport-security-options.json");
+ private static final TransportSecurityOptions OPTIONS = new TransportSecurityOptions.Builder()
+ .withCertificate(Paths.get("certs.pem"), Paths.get("myhost.key"))
+ .withCaCertificate(Paths.get("my_cas.pem"))
+ .build();
@Test
public void can_read_options_from_json_file() {
- TransportSecurityOptions expectedOptions = new TransportSecurityOptions("myhost.key", "certs.pem", "my_cas.pem");
TransportSecurityOptions actualOptions = TransportSecurityOptions.fromJsonFile(TEST_CONFIG_FILE);
- assertEquals(expectedOptions, actualOptions);
+ assertEquals(OPTIONS, actualOptions);
}
@Test
public void can_read_options_from_json() throws IOException {
String tlsJson = new String(Files.readAllBytes(TEST_CONFIG_FILE), StandardCharsets.UTF_8);
- TransportSecurityOptions expectedOptions = new TransportSecurityOptions("myhost.key", "certs.pem", "my_cas.pem");
TransportSecurityOptions actualOptions = TransportSecurityOptions.fromJson(tlsJson);
- assertEquals(expectedOptions, actualOptions);
+ assertEquals(OPTIONS, actualOptions);
}
}
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java
new file mode 100644
index 00000000000..a3e5bef115b
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java
@@ -0,0 +1,46 @@
+package com.yahoo.security.tls.json;
+
+import com.yahoo.security.tls.TransportSecurityOptions;
+import com.yahoo.security.tls.policy.AuthorizedPeers;
+import com.yahoo.security.tls.policy.HostGlobPattern;
+import com.yahoo.security.tls.policy.PeerPolicy;
+import com.yahoo.security.tls.policy.RequiredPeerCredential;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.*;
+import static org.junit.Assert.*;
+
+/**
+ * @author bjorncs
+ */
+public class TransportSecurityOptionsJsonSerializerTest {
+
+ @Test
+ public void can_serialize_and_deserialize_transport_security_options() {
+ TransportSecurityOptions options = new TransportSecurityOptions.Builder()
+ .withCaCertificate(Paths.get("/path/to/ca-certs.pem"))
+ .withCertificate(Paths.get("/path/to/cert.pem"), Paths.get("/path/to/key.pem"))
+ .withAuthorizedPeers(
+ new AuthorizedPeers(
+ new HashSet<>(Arrays.asList(
+ new PeerPolicy("cfgserver", Arrays.asList(
+ new RequiredPeerCredential(CN, new HostGlobPattern("mycfgserver")),
+ new RequiredPeerCredential(SAN_DNS, new HostGlobPattern("*.suffix.com")))),
+ new PeerPolicy("node", Collections.singletonList(new RequiredPeerCredential(CN, new HostGlobPattern("hostname"))))))))
+ .build();
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ TransportSecurityOptionsJsonSerializer serializer = new TransportSecurityOptionsJsonSerializer();
+ serializer.serialize(out, options);
+ TransportSecurityOptions deserializedOptions = serializer.deserialize(new ByteArrayInputStream(out.toByteArray()));
+ assertEquals(options, deserializedOptions);
+ }
+
+} \ No newline at end of file