diff options
author | Martin Polden <mpolden@mpolden.no> | 2019-09-24 09:12:20 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-09-24 09:12:20 +0200 |
commit | a975345f861f8560def95fa3e92364ecaa0bd225 (patch) | |
tree | a5a7f41b23073f1c9f1ef8e1d2196be4ed43c549 | |
parent | 85a1670715c0cf8c83302c5423a78f42dfc41cda (diff) | |
parent | e241ebcd59fcaab4749be542b16aaff590628e52 (diff) |
Merge pull request #10771 from vespa-engine/mpolden/refresh-cert
Implement refresh instance
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); |