aboutsummaryrefslogtreecommitdiffstats
path: root/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java
blob: 40a23ab590441645369fc109d7585db094997110 (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
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.athenz.identityprovider.client;

import com.yahoo.container.core.identity.IdentityConfig;
import com.yahoo.security.KeyAlgorithm;
import com.yahoo.security.KeyUtils;
import com.yahoo.security.Pkcs10Csr;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
import com.yahoo.vespa.athenz.client.zts.InstanceIdentity;
import com.yahoo.vespa.athenz.client.zts.ZtsClient;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument;
import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient;
import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
import com.yahoo.vespa.athenz.utils.SiaUtils;
import com.yahoo.vespa.defaults.Defaults;

import javax.net.ssl.SSLContext;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
import java.util.Optional;

import static java.util.Collections.singleton;

/**
 * A service that provides method for initially registering the instance and refreshing it.
 *
 * @author bjorncs
 */
class AthenzCredentialsService {
    private static final Duration EXPIRATION_MARGIN = Duration.ofDays(2);
    private static final Path VESPA_SIA_DIRECTORY = Paths.get(Defaults.getDefaults().underVespaHome("var/vespa/sia"));
    private static final Path IDENTITY_DOCUMENT_FILE = VESPA_SIA_DIRECTORY.resolve("vespa-tenant-identity-document.json");

    private final AthenzService tenantIdentity;
    private final URI configserverEndpoint;
    private final URI ztsEndpoint;
    private final AthenzService configserverIdentity;
    private final ServiceIdentityProvider nodeIdentityProvider;
    private final String hostname;
    private final CsrGenerator csrGenerator;
    private final Clock clock;

    AthenzCredentialsService(IdentityConfig identityConfig,
                             ServiceIdentityProvider nodeIdentityProvider,
                             String hostname,
                             Clock clock) {
        this.tenantIdentity = new AthenzService(identityConfig.domain(), identityConfig.service());
        this.configserverEndpoint = URI.create("https://" + identityConfig.loadBalancerAddress() + ":4443");
        this.ztsEndpoint = URI.create(identityConfig.ztsUrl());
        this.configserverIdentity = new AthenzService(identityConfig.configserverIdentityName());
        this.nodeIdentityProvider = nodeIdentityProvider;
        this.hostname = hostname;
        this.csrGenerator = new CsrGenerator(identityConfig.athenzDnsSuffix(), identityConfig.configserverIdentityName());
        this.clock = clock;
    }

    Path certificatePath() { return SiaUtils.getCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity); }
    Path privateKeyPath() { return SiaUtils.getPrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity); }

    AthenzCredentials registerInstance() {
        Optional<AthenzCredentials> athenzCredentialsFromDisk = tryReadCredentialsFromDisk();
        if (athenzCredentialsFromDisk.isPresent()) {
            return athenzCredentialsFromDisk.get();
        }
        KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
        IdentityDocumentClient identityDocumentClient = createIdentityDocumentClient();
        // Use legacy version for now.
        SignedIdentityDocument signedDocument = identityDocumentClient.getTenantIdentityDocument(hostname, SignedIdentityDocument.LEGACY_DEFAULT_DOCUMENT_VERSION).orElseThrow();
        IdentityDocument document = signedDocument.identityDocument();
        Pkcs10Csr csr = csrGenerator.generateInstanceCsr(
                tenantIdentity,
                document.providerUniqueId(),
                document.ipAddresses(),
                document.clusterType(),
                keyPair);

        try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withIdentityProvider(nodeIdentityProvider).build()) {
            InstanceIdentity instanceIdentity =
                    ztsClient.registerInstance(
                            configserverIdentity,
                            tenantIdentity,
                            EntityBindingsMapper.toAttestationData(signedDocument),
                            csr);
            X509Certificate certificate = instanceIdentity.certificate();
            writeCredentialsToDisk(keyPair.getPrivate(), certificate, signedDocument);
            return new AthenzCredentials(certificate, keyPair, signedDocument);
        }
    }

    AthenzCredentials updateCredentials(SignedIdentityDocument signedDocument, SSLContext sslContext) {
        KeyPair newKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
        IdentityDocument document = signedDocument.identityDocument();
        Pkcs10Csr csr = csrGenerator.generateInstanceCsr(
                tenantIdentity,
                document.providerUniqueId(),
                document.ipAddresses(),
                document.clusterType(),
                newKeyPair);

        try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(sslContext).build()) {
            InstanceIdentity instanceIdentity =
                    ztsClient.refreshInstance(
                            configserverIdentity,
                            tenantIdentity,
                            document.providerUniqueId().asDottedString(),
                            csr);
            X509Certificate certificate = instanceIdentity.certificate();
            writeCredentialsToDisk(newKeyPair.getPrivate(), certificate, signedDocument);
            return new AthenzCredentials(certificate, newKeyPair, signedDocument);
        }
    }

    private Optional<AthenzCredentials> tryReadCredentialsFromDisk() {
        Optional<PrivateKey> privateKey = SiaUtils.readPrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity);
        if (!privateKey.isPresent()) return Optional.empty();
        Optional<X509Certificate> certificate = SiaUtils.readCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity);
        if (!certificate.isPresent()) return Optional.empty();
        if (isExpired(certificate.get())) {
            return Optional.empty();
        }
        if (Files.notExists(IDENTITY_DOCUMENT_FILE)) return Optional.empty();
        SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.readSignedIdentityDocumentFromFile(IDENTITY_DOCUMENT_FILE);
        KeyPair keyPair = new KeyPair(KeyUtils.extractPublicKey(privateKey.get()), privateKey.get());
        return Optional.of(new AthenzCredentials(certificate.get(), keyPair, signedIdentityDocument));
    }

    private boolean isExpired(X509Certificate certificate) {
        return clock.instant().isAfter(certificate.getNotAfter().toInstant().minus(EXPIRATION_MARGIN));
    }

    private void writeCredentialsToDisk(PrivateKey privateKey,
                                       X509Certificate certificate,
                                       SignedIdentityDocument identityDocument) {
        SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity, privateKey);
        SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity, certificate);
        EntityBindingsMapper.writeSignedIdentityDocumentToFile(IDENTITY_DOCUMENT_FILE, identityDocument);
    }

    private DefaultIdentityDocumentClient createIdentityDocumentClient() {
        return new DefaultIdentityDocumentClient(
                configserverEndpoint,
                nodeIdentityProvider,
                new AthenzIdentityVerifier(singleton(configserverIdentity)));
    }
}