diff options
Diffstat (limited to 'athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java')
-rw-r--r-- | athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java | 72 |
1 files changed, 69 insertions, 3 deletions
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 1b9bdcdb987..4c01b0943e4 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 @@ -6,16 +6,24 @@ import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.container.jdisc.secretstore.SecretStore; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import com.yahoo.log.LogLevel; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.Path; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.security.KeyUtils; +import com.yahoo.security.SubjectAlternativeName; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Slime; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceConfirmation; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator; import com.yahoo.vespa.hosted.ca.Certificates; import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; +import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh; import com.yahoo.yolean.Exceptions; import java.io.IOException; @@ -23,6 +31,10 @@ import java.io.UncheckedIOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Clock; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; @@ -43,18 +55,20 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private final Certificates certificates; private final String caPrivateKeySecretName; private final String caCertificateSecretName; + private final InstanceValidator instanceValidator; @Inject - public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig) { - this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig); + public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) { + this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig, instanceValidator); } - CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig) { + CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) { super(ctx); this.secretStore = secretStore; this.certificates = certificates; this.caPrivateKeySecretName = athenzProviderServiceConfig.secretName(); this.caCertificateSecretName = athenzProviderServiceConfig.domain() + ".ca.cert"; + this.instanceValidator = instanceValidator; } @Override @@ -81,6 +95,14 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private HttpResponse registerInstance(HttpRequest request) { var instanceRegistration = deserializeRequest(request, InstanceSerializer::registrationFromSlime); + + InstanceConfirmation confirmation = new InstanceConfirmation(instanceRegistration.provider(), instanceRegistration.domain(), instanceRegistration.service(), EntityBindingsMapper.toSignedIdentityDocumentEntity(instanceRegistration.attestationData())); + confirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); + confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.DNS_NAME)); + if (!instanceValidator.isValidInstance(confirmation)) { + log.log(LogLevel.INFO, "Invalid instance registration for " + instanceRegistration.toString()); + return ErrorResponse.forbidden("Unable to launch service: " +instanceRegistration.service()); + } var certificate = certificates.create(instanceRegistration.csr(), caCertificate(), caPrivateKey()); var instanceId = Certificates.instanceIdFrom(instanceRegistration.csr()); var identity = new InstanceIdentity(instanceRegistration.provider(), instanceRegistration.service(), instanceId, @@ -91,21 +113,65 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) { var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime); var instanceIdFromCsr = Certificates.instanceIdFrom(instanceRefresh.csr()); + var athenzService = new AthenzService(request.getJDiscRequest().getUserPrincipal().getName()); if (!instanceIdFromCsr.equals(instanceId)) { throw new IllegalArgumentException("Mismatch between instance ID in URL path and instance ID in CSR " + "[instanceId=" + instanceId + ",instanceIdFromCsr=" + instanceIdFromCsr + "]"); } + + // Verify that the csr instance id matches one of the certificates in the chain + refreshesSameInstanceId(instanceIdFromCsr, request); + + + // Validate that there is no privilege escalation (can only refresh same service) + refreshesSameService(instanceRefresh, athenzService); + + InstanceConfirmation instanceConfirmation = new InstanceConfirmation(provider, athenzService.getDomain().getName(), athenzService.getName(), null); + instanceConfirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); + instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.DNS_NAME)); + if(!instanceValidator.isValidRefresh(instanceConfirmation)) { + return ErrorResponse.forbidden("Unable to refresh cert: " + instanceRefresh.csr().getSubject().toString()); + } + var certificate = certificates.create(instanceRefresh.csr(), caCertificate(), caPrivateKey()); var identity = new InstanceIdentity(provider, service, instanceIdFromCsr, Optional.of(certificate)); return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity)); } + public void refreshesSameInstanceId(String csrInstanceId, HttpRequest request) { + String certificateInstanceId = getRequestCertificateChain(request).stream() + .map(Certificates::instanceIdFrom) + .filter(Optional::isPresent) + .map(Optional::get) + .findAny().orElseThrow(() -> new IllegalArgumentException("No client certificate with instance id in request.")); + + if(! Objects.equals(certificateInstanceId, csrInstanceId)) { + throw new IllegalArgumentException("Mismatch between instance ID in client certificate and instance ID in CSR " + + "[instanceId=" + certificateInstanceId + ",instanceIdFromCsr=" + csrInstanceId + + "]"); + } + } + + private void refreshesSameService(InstanceRefresh instanceRefresh, AthenzService athenzService) { + List<String> commonNames = X509CertificateUtils.getCommonNames(instanceRefresh.csr().getSubject()); + if(commonNames.size() != 1 && !Objects.equals(commonNames.get(0), athenzService.getFullName())) { + throw new IllegalArgumentException(String.format("Invalid request, trying to refresh service %s using service %s.", instanceRefresh.csr().getSubject().getName(), athenzService.getFullName())); + } + } + /** Returns CA certificate from secret store */ private X509Certificate caCertificate() { return X509CertificateUtils.fromPem(secretStore.getSecret(caCertificateSecretName)); } + private List<X509Certificate> getRequestCertificateChain(HttpRequest request) { + return Optional.ofNullable(request.getJDiscRequest().context().get(ServletRequest.JDISC_REQUEST_X509CERT)) + .map(X509Certificate[].class::cast) + .map(Arrays::asList) + .orElse(Collections.emptyList()); + } + /** Returns CA private key from secret store */ private PrivateKey caPrivateKey() { return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(caPrivateKeySecretName)); |