diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-11-19 16:37:32 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-11-19 16:37:35 +0100 |
commit | 64ad086cb1789f1881ff44d1432c905fecc07e41 (patch) | |
tree | 6e511dffa8039e3749303053d8fa3be7c4bd882b /security-utils | |
parent | aa8dfde70a0c045629d645f49c50a2963f8ff66a (diff) |
Rewrite JSON serialization of TransportSecurityOptions
- Use Jackson data bindings on TransportSecurityOptionsEntity
- Add serialization to JSON
- Add AuthorizedPeers to TransportSecurityOptions
Diffstat (limited to 'security-utils')
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 |