summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2019-09-24 09:12:20 +0200
committerGitHub <noreply@github.com>2019-09-24 09:12:20 +0200
commita975345f861f8560def95fa3e92364ecaa0bd225 (patch)
treea5a7f41b23073f1c9f1ef8e1d2196be4ed43c549
parent85a1670715c0cf8c83302c5423a78f42dfc41cda (diff)
parente241ebcd59fcaab4749be542b16aaff590628e52 (diff)
Merge pull request #10771 from vespa-engine/mpolden/refresh-cert
Implement refresh instance
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java2
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java40
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java5
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java30
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java6
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java54
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java11
7 files changed, 128 insertions, 20 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java
index b499debcc47..25c4cbb2281 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java
@@ -7,7 +7,7 @@ import java.util.Optional;
/**
* A signed instance identity object that includes a client certificate. This is the result of a successful
- * {@link InstanceRegistration}.
+ * {@link InstanceRegistration} and is the same type as InstanceIdentity in the ZTS API.
*
* @author mpolden
*/
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java
new file mode 100644
index 00000000000..fbcda5e68cb
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java
@@ -0,0 +1,40 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.instance;
+
+import com.yahoo.security.Pkcs10Csr;
+
+import java.util.Objects;
+
+/**
+ * Information for refreshing a instance in the system. This is the same type as InstanceRefreshInformation type in
+ * the ZTS API.
+ *
+ * @author mpolden
+ */
+public class InstanceRefresh {
+
+ private final Pkcs10Csr csr;
+
+ public InstanceRefresh(Pkcs10Csr csr) {
+ this.csr = Objects.requireNonNull(csr, "csr must be non-null");
+ }
+
+ /** The Certificate Signed Request describing the wanted certificate */
+ public Pkcs10Csr csr() {
+ return csr;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceRefresh that = (InstanceRefresh) o;
+ return csr.equals(that.csr);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(csr);
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java
index 7a9ec74e075..2a2b702d21b 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java
@@ -6,8 +6,8 @@ import com.yahoo.security.Pkcs10Csr;
import java.util.Objects;
/**
- * Information for registering a new instance in the system. This is similar to the InstanceRegisterInformation type in
- * ZTS.
+ * Information for registering a new instance in the system. This is the same type as InstanceRegisterInformation type
+ * in the ZTS API.
*
* @author mpolden
*/
@@ -47,6 +47,7 @@ public class InstanceRegistration {
return attestationData;
}
+ /** The Certificate Signed Request describing the wanted certificate */
public Pkcs10Csr csr() {
return csr;
}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
index ba4f0ce932c..b2120f24160 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
@@ -25,12 +25,14 @@ import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.util.Optional;
+import java.util.function.Function;
import java.util.logging.Level;
/**
* REST API for issuing and refreshing node certificates in a hosted Vespa system.
*
* The API implements the following subset of methods from the Athenz ZTS REST API:
+ *
* - Instance registration
* - Instance refresh
*
@@ -59,12 +61,12 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
try {
switch (request.getMethod()) {
case POST: return handlePost(request);
- default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
+ default: return ErrorResponse.methodNotAllowed("Method " + request.getMethod() + " is unsupported");
}
} catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ return ErrorResponse.badRequest(request.getMethod() + " " + request.getUri() + " failed: " + Exceptions.toMessageString(e));
} catch (RuntimeException e) {
- log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ log.log(Level.WARNING, "Unexpected error handling " + request.getMethod() + " " + request.getUri(), e);
return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
}
}
@@ -72,13 +74,12 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
private HttpResponse handlePost(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/ca/v1/instance/")) return registerInstance(request);
- // TODO: Implement refresh
+ if (path.matches("/ca/v1/instance/{provider}/{domain}/{service}/{instanceId}")) return refreshInstance(request, path.get("provider"), path.get("service"), path.get("instanceId"));
return ErrorResponse.notFoundError("Nothing at " + path);
}
private HttpResponse registerInstance(HttpRequest request) {
- var body = slimeFromRequest(request);
- var instanceRegistration = InstanceSerializer.registrationFromSlime(body);
+ var instanceRegistration = deserializeRequest(request, InstanceSerializer::registrationFromSlime);
var certificate = certificates.create(instanceRegistration.csr(), caCertificate(), caPrivateKey());
var instanceId = Certificates.extractDnsName(instanceRegistration.csr());
var identity = new InstanceIdentity(instanceRegistration.provider(), instanceRegistration.service(), instanceId,
@@ -86,6 +87,18 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity));
}
+ private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) {
+ var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime);
+ var instanceIdFromCsr = Certificates.extractDnsName(instanceRefresh.csr());
+ if (!instanceIdFromCsr.equals(instanceId)) {
+ throw new IllegalArgumentException("Mismatched instance ID and SAN DNS name [instanceId=" + instanceId +
+ ",dnsName=" + instanceIdFromCsr + "]");
+ }
+ var certificate = certificates.create(instanceRefresh.csr(), caCertificate(), caPrivateKey());
+ var identity = new InstanceIdentity(provider, service, instanceIdFromCsr, Optional.of(certificate));
+ return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity));
+ }
+
/** Returns CA certificate from secret store */
private X509Certificate caCertificate() {
var keyName = String.format("vespa.external.%s.configserver.ca.cert.cert", system.value().toLowerCase());
@@ -98,9 +111,10 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(keyName));
}
- private static Slime slimeFromRequest(HttpRequest request) {
+ private static <T> T deserializeRequest(HttpRequest request, Function<Slime, T> serializer) {
try {
- return SlimeUtils.jsonToSlime(request.getData().readAllBytes());
+ var slime = SlimeUtils.jsonToSlime(request.getData().readAllBytes());
+ return serializer.apply(slime);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java
index 46a09e9c6f2..a2537cd68f1 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java
@@ -6,6 +6,7 @@ import com.yahoo.security.X509CertificateUtils;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration;
/**
@@ -33,6 +34,11 @@ public class InstanceSerializer {
Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString()));
}
+ public static InstanceRefresh refreshFromSlime(Slime slime) {
+ Cursor root = slime.get();
+ return new InstanceRefresh(Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString()));
+ }
+
public static Slime identityToSlime(InstanceIdentity identity) {
Slime slime = new Slime();
Cursor root = slime.setObject();
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java
index 4393c3a25b9..a1d708a1107 100644
--- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java
@@ -35,9 +35,9 @@ public class CertificateAuthorityApiTest extends ContainerTester {
public void register_instance() throws Exception {
// POST instance registration
var csr = CertificateTester.createCsr("node1.example.com");
- assertRegistration(new Request("http://localhost:12345/ca/v1/instance/",
- instanceRegistrationJson(csr),
- Request.Method.POST));
+ assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/",
+ instanceRegistrationJson(csr),
+ Request.Method.POST));
// POST instance registration with ZTS client
var ztsClient = new DefaultZtsClient(URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
@@ -49,9 +49,26 @@ public class CertificateAuthorityApiTest extends ContainerTester {
}
@Test
- public void invalid_register_instance() {
+ public void refresh_instance() throws Exception {
+ // POST instance refresh
+ var csr = CertificateTester.createCsr("node1.example.com");
+ assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/node1.example.com",
+ instanceRefreshJson(csr),
+ Request.Method.POST));
+
+ // POST instance refresh with ZTS client
+ var ztsClient = new DefaultZtsClient(URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
+ var instanceIdentity = ztsClient.refreshInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"),
+ new AthenzService("vespa.external", "tenant"),
+ "node1.example.com",
+ csr);
+ assertEquals("CN=Vespa CA", instanceIdentity.certificate().getIssuerX500Principal().getName());
+ }
+
+ @Test
+ public void invalid_requests() {
// POST instance registration with missing fields
- assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Missing required field 'provider'\"}",
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/ failed: Missing required field 'provider'\"}",
new Request("http://localhost:12345/ca/v1/instance/",
new byte[0],
Request.Method.POST));
@@ -61,7 +78,20 @@ public class CertificateAuthorityApiTest extends ContainerTester {
var request = new Request("http://localhost:12345/ca/v1/instance/",
instanceRegistrationJson(csr),
Request.Method.POST);
- assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"DNS name not found in CSR\"}", request);
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/ failed: DNS name not found in CSR\"}", request);
+
+ // POST instance refresh with missing field
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/node1.example.com failed: Missing required field 'csr'\"}",
+ new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/node1.example.com",
+ new byte[0],
+ Request.Method.POST));
+
+ // POST instance refresh where instanceId does not match CSR dnsName
+ csr = CertificateTester.createCsr("node1.example.com");
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/node2.example.com failed: Mismatched instance ID and SAN DNS name [instanceId=node2.example.com,dnsName=node1.example.com]\"}",
+ new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/node2.example.com",
+ instanceRefreshJson(csr),
+ Request.Method.POST));
}
private void setCaCertificateAndKey() {
@@ -72,11 +102,11 @@ public class CertificateAuthorityApiTest extends ContainerTester {
.setSecret("vespa.external.main.configserver.ca.key.key", privateKeyPem);
}
- private void assertRegistration(Request request) {
+ private void assertIdentityResponse(Request request) {
assertResponse(200, (body) -> {
var slime = SlimeUtils.jsonToSlime(body);
var root = slime.get();
- assertEquals("provider_prod_us-north-1", root.field("provider").asString());
+ assertEquals("vespa.external.provider_prod_us-north-1", root.field("provider").asString());
assertEquals("tenant", root.field("service").asString());
assertEquals("node1.example.com", root.field("instanceId").asString());
var pemEncodedCertificate = root.field("x509Certificate").asString();
@@ -86,10 +116,16 @@ public class CertificateAuthorityApiTest extends ContainerTester {
}, request);
}
+ private static byte[] instanceRefreshJson(Pkcs10Csr csr) {
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ var json = "{\"csr\": \"" + csrPem + "\"}";
+ return json.getBytes(StandardCharsets.UTF_8);
+ }
+
private static byte[] instanceRegistrationJson(Pkcs10Csr csr) {
var csrPem = Pkcs10CsrUtils.toPem(csr);
var json = "{\n" +
- " \"provider\": \"provider_prod_us-north-1\",\n" +
+ " \"provider\": \"vespa.external.provider_prod_us-north-1\",\n" +
" \"domain\": \"vespa.external\",\n" +
" \"service\": \"tenant\",\n" +
" \"attestationData\": \"identity document generated by config server\",\n" +
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java
index 51010422b6d..83ea9249ad0 100644
--- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java
@@ -7,6 +7,7 @@ import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.ca.CertificateTester;
import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration;
import org.junit.Test;
@@ -55,6 +56,16 @@ public class InstanceSerializerTest {
assertEquals(json, asJsonString(InstanceSerializer.identityToSlime(identity)));
}
+ @Test
+ public void serialize_instance_refresh() {
+ var csr = CertificateTester.createCsr();
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ var json = "{\"csr\": \"" + csrPem + "\"}";
+ var instanceRefresh = new InstanceRefresh(csr);
+ var deserialized = InstanceSerializer.refreshFromSlime(SlimeUtils.jsonToSlime(json));
+ assertEquals(instanceRefresh, deserialized);
+ }
+
private static String asJsonString(Slime slime) {
try {
return new String(SlimeUtils.toJsonBytes(slime), StandardCharsets.UTF_8);