aboutsummaryrefslogtreecommitdiffstats
path: root/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@yahooinc.com>2022-07-19 14:45:41 +0200
committerBjørn Christian Seime <bjorncs@yahooinc.com>2022-07-20 13:56:33 +0200
commit91b46555d137dcdf73a534ba5fa10e07510eb0f9 (patch)
treeb5d84e0d32f784512ec5fd70c889639828ad8e58 /security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java
parentff025feea342cabb764b8e9cc1bba34cafe09409 (diff)
Merge Java package 'c.y.s.tls.{auth,json,policy}' into 'c.y.s.tls'
Facilitate improved encapsulation of Vespa mTLS related classes
Diffstat (limited to 'security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java187
1 files changed, 187 insertions, 0 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java
new file mode 100644
index 00000000000..0349d4085db
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java
@@ -0,0 +1,187 @@
+// Copyright Yahoo. 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.ObjectMapper;
+import com.yahoo.security.tls.TransportSecurityOptionsEntity.AuthorizedPeer;
+import com.yahoo.security.tls.TransportSecurityOptionsEntity.CredentialField;
+import com.yahoo.security.tls.TransportSecurityOptionsEntity.Files;
+import com.yahoo.security.tls.TransportSecurityOptionsEntity.RequiredCredential;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * @author bjorncs
+ */
+class TransportSecurityOptionsJsonSerializer {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ TransportSecurityOptions deserialize(InputStream in) {
+ try {
+ TransportSecurityOptionsEntity entity = mapper.readValue(in, TransportSecurityOptionsEntity.class);
+ return toTransportSecurityOptions(entity);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ void serialize(OutputStream out, TransportSecurityOptions options) {
+ try {
+ mapper.writerWithDefaultPrettyPrinter().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.withCertificates(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.withCaCertificates(Paths.get(files.caCertificatesFile));
+ }
+ }
+ List<AuthorizedPeer> authorizedPeersEntity = entity.authorizedPeers;
+ if (authorizedPeersEntity != null) {
+ if (authorizedPeersEntity.size() == 0) {
+ throw new IllegalArgumentException("'authorized-peers' cannot be empty");
+ }
+ builder.withAuthorizedPeers(new AuthorizedPeers(toPeerPolicies(authorizedPeersEntity)));
+ }
+ if (entity.acceptedCiphers != null) {
+ if (entity.acceptedCiphers.isEmpty()) {
+ throw new IllegalArgumentException("'accepted-ciphers' cannot be empty");
+ }
+ builder.withAcceptedCiphers(entity.acceptedCiphers);
+ }
+ if (entity.acceptedProtocols != null) {
+ if (entity.acceptedProtocols.isEmpty()) {
+ throw new IllegalArgumentException("'accepted-protocols' cannot be empty");
+ }
+ builder.withAcceptedProtocols(entity.acceptedProtocols);
+ }
+ if (entity.isHostnameValidationDisabled != null) {
+ builder.withHostnameValidationDisabled(entity.isHostnameValidationDisabled);
+ }
+ 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 == null) {
+ throw missingFieldException("required-credentials");
+ }
+ return new PeerPolicy(authorizedPeer.name, Optional.ofNullable(authorizedPeer.description),
+ toCapabilities(authorizedPeer.capabilities), toRequestPeerCredentials(authorizedPeer.requiredCredentials));
+ }
+
+ private static CapabilitySet toCapabilities(List<String> capabilities) {
+ if (capabilities == null) return CapabilitySet.all();
+ if (capabilities.isEmpty())
+ throw new IllegalArgumentException("\"capabilities\" array must either be not present " +
+ "(implies all capabilities) or contain at least one capability name");
+ return CapabilitySet.fromNames(capabilities);
+ }
+
+ 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 missingFieldException("field");
+ }
+ if (requiredCredential.matchExpression == null) {
+ throw missingFieldException("must-match");
+ }
+ return RequiredPeerCredential.of(toField(requiredCredential.field), 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;
+ case SAN_URI: return RequiredPeerCredential.Field.SAN_URI;
+ 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());
+ entity.authorizedPeers = options.getAuthorizedPeers().peerPolicies().stream()
+ // Makes tests stable
+ .sorted(Comparator.comparing(PeerPolicy::policyName))
+ .map(peerPolicy -> {
+ AuthorizedPeer authorizedPeer = new AuthorizedPeer();
+ authorizedPeer.name = peerPolicy.policyName();
+ authorizedPeer.requiredCredentials = new ArrayList<>();
+ authorizedPeer.description = peerPolicy.description().orElse(null);
+ CapabilitySet caps = peerPolicy.capabilities();
+ if (!caps.hasAll()) {
+ authorizedPeer.capabilities = List.copyOf(caps.toNames());
+ }
+ for (RequiredPeerCredential requiredPeerCredential : peerPolicy.requiredCredentials()) {
+ RequiredCredential requiredCredential = new RequiredCredential();
+ requiredCredential.field = toField(requiredPeerCredential.field());
+ requiredCredential.matchExpression = requiredPeerCredential.pattern().asString();
+ authorizedPeer.requiredCredentials.add(requiredCredential);
+ }
+ return authorizedPeer;
+ })
+ .toList();
+ if (!options.getAcceptedCiphers().isEmpty()) {
+ entity.acceptedCiphers = options.getAcceptedCiphers();
+ }
+ if (!options.getAcceptedProtocols().isEmpty()) {
+ entity.acceptedProtocols = options.getAcceptedProtocols();
+ }
+ if (options.isHostnameValidationDisabled()) {
+ entity.isHostnameValidationDisabled = true;
+ }
+ return entity;
+ }
+
+ private static CredentialField toField(RequiredPeerCredential.Field field) {
+ switch (field) {
+ case CN: return CredentialField.CN;
+ case SAN_DNS: return CredentialField.SAN_DNS;
+ case SAN_URI: return CredentialField.SAN_URI;
+ default: throw new IllegalArgumentException("Invalid field type: " + field);
+ }
+ }
+
+ private static IllegalArgumentException missingFieldException(String fieldName) {
+ return new IllegalArgumentException(String.format("'%s' missing", fieldName));
+ }
+}