aboutsummaryrefslogtreecommitdiffstats
path: root/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java
blob: 66b90b32f79c532605d55d6f11a5f52a1cf22f71 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// 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.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 Set<String> toCapabilities(List<String> capabilities) {
        if (capabilities == null) return Set.of(CapabilitySet.ALL.toPredefinedName().get());
        if (capabilities.isEmpty())
            throw new IllegalArgumentException("\"capabilities\" array must either be not present " +
                    "(implies all capabilities) or contain at least one capability name");
        return Set.copyOf(capabilities);
    }

    private static List<RequiredPeerCredential> toRequestPeerCredentials(List<RequiredCredential> requiredCredentials) {
        return requiredCredentials.stream()
                .map(TransportSecurityOptionsJsonSerializer::toRequiredPeerCredential)
                .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 = peerPolicy.capabilityNames().stream().sorted().toList();
                    }
                    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));
    }
}