diff options
author | Morten Tokle <mortent@verizonmedia.com> | 2019-10-29 09:37:50 +0100 |
---|---|---|
committer | Morten Tokle <mortent@verizonmedia.com> | 2019-10-29 09:53:45 +0100 |
commit | 16389caf12181937f2c4d66f2f62f565e0c5ff12 (patch) | |
tree | f501d1004820e251eb8366b5dffbe353ac2aeee5 /athenz-identity-provider-service/src/main | |
parent | cd8e24124d02ccfcbffc843115ddffecca20cbe4 (diff) |
Validate register and refresh
Diffstat (limited to 'athenz-identity-provider-service/src/main')
4 files changed, 97 insertions, 11 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java index f1a93e58526..2eae26a814d 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java @@ -40,6 +40,9 @@ public class InstanceValidator { static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service"; static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz."; + public static final String SAN_IPS_ATTRNAME = "sanIP"; + public static final String SAN_DNS_ATTRNAME = "sanDNS"; + private final IdentityDocumentSigner signer; private final KeyProvider keyProvider; private final SuperModelProvider superModelProvider; @@ -96,7 +99,7 @@ public class InstanceValidator { log.log(LogLevel.INFO, () -> String.format("Accepting refresh for instance with identity '%s', provider '%s', instanceId '%s'.", new AthenzService(confirmation.domain, confirmation.service).getFullName(), confirmation.provider, - confirmation.attributes.get("sanDNS"))); + confirmation.attributes.get(SAN_DNS_ATTRNAME))); try { return validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); } catch (Exception e) { @@ -107,7 +110,7 @@ public class InstanceValidator { private VespaUniqueInstanceId getVespaUniqueInstanceId(InstanceConfirmation instanceConfirmation) { // Find a list of SAN DNS - List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get("sanDNS")) + List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME)) .map(s -> s.split(",")) .map(Arrays::asList) .map(List::stream) @@ -124,7 +127,7 @@ public class InstanceValidator { private boolean validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) { if(vespaUniqueInstanceId == null) { - log.log(LogLevel.WARNING, "Unabe to find unique instance ID in refresh request: " + confirmation.toString()); + log.log(LogLevel.WARNING, "Unable to find unique instance ID in refresh request: " + confirmation.toString()); return false; } @@ -140,7 +143,7 @@ public class InstanceValidator { } // Find list of ipaddresses - List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get("sanIP")) + List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get(SAN_IPS_ATTRNAME)) .map(s -> s.split(",")) .map(Arrays::asList) .map(List::stream) 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 2a2b702d21b..8d9f44e4564 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 @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.ca.instance; import com.yahoo.security.Pkcs10Csr; +import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import java.util.Objects; @@ -16,10 +17,10 @@ public class InstanceRegistration { private final String provider; private final String domain; private final String service; - private final String attestationData; + private final SignedIdentityDocument attestationData; private final Pkcs10Csr csr; - public InstanceRegistration(String provider, String domain, String service, String attestationData, Pkcs10Csr csr) { + public InstanceRegistration(String provider, String domain, String service, SignedIdentityDocument attestationData, Pkcs10Csr csr) { this.provider = Objects.requireNonNull(provider, "provider must be non-null"); this.domain = Objects.requireNonNull(domain, "domain must be non-null"); this.service = Objects.requireNonNull(service, "service must be non-null"); @@ -43,7 +44,7 @@ public class InstanceRegistration { } /** Host document describing this instance (received from config server) */ - public String attestationData() { + public SignedIdentityDocument attestationData() { return attestationData; } 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..10d4c2fb0d9 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,14 +6,21 @@ 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.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.Pkcs10Csr; +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.yolean.Exceptions; @@ -23,9 +30,12 @@ import java.io.UncheckedIOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Clock; +import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; +import java.util.stream.Collectors; /** * REST API for issuing and refreshing node certificates in a hosted Vespa system. @@ -43,18 +53,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 +93,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, getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); + confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, 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, @@ -88,6 +108,14 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity)); } + private String getSubjectAlternativeNames(Pkcs10Csr csr, SubjectAlternativeName.Type sanType) { + return csr.getSubjectAlternativeNames().stream() + .map(SubjectAlternativeName::decode) + .filter(san -> san.getType() == sanType) + .map(SubjectAlternativeName::getValue) + .collect(Collectors.joining(",")); + } + private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) { var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime); var instanceIdFromCsr = Certificates.instanceIdFrom(instanceRefresh.csr()); @@ -96,6 +124,18 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler { "[instanceId=" + instanceId + ",instanceIdFromCsr=" + instanceIdFromCsr + "]"); } + AthenzService athenzService = new AthenzService(request.getJDiscRequest().getUserPrincipal().getName()); + 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())); + } + InstanceConfirmation instanceConfirmation = new InstanceConfirmation(provider, athenzService.getDomain().getName(), athenzService.getName(), null); + instanceConfirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.IP_ADDRESS)); + instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, 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)); 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 a2537cd68f1..f08e5e37417 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 @@ -3,12 +3,23 @@ package com.yahoo.vespa.hosted.ca.restapi; import com.yahoo.security.Pkcs10CsrUtils; import com.yahoo.security.X509CertificateUtils; +import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; +import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; +import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; +import com.yahoo.vespa.config.SlimeUtils; 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 java.time.Instant; +import java.util.HashSet; +import java.util.Set; + /** * @author mpolden */ @@ -23,6 +34,17 @@ public class InstanceSerializer { private static final String INSTANCE_ID_FIELD = "instanceId"; private static final String X509_CERTIFICATE_FIELD = "x509Certificate"; + private static final String IDD_SIGNATURE_FIELD = "signature"; + private static final String IDD_SIGNING_KEY_VERSION_FIELD = "signing-key-version"; + private static final String IDD_PROVIDER_UNIQUE_ID_FIELD = "provider-unique-id"; + private static final String IDD_PROVIDER_SERVICE_FIELD = "provider-service"; + private static final String IDD_DOCUMENT_VERSION_FIELD = "document-version"; + private static final String IDD_CONFIGSERVER_HOSTNAME_FIELD = "configserver-hostname"; + private static final String IDD_INSTANCE_HOSTNAME_FIELD = "instance-hostname"; + private static final String IDD_CREATED_AT_FIELD = "created-at"; + private static final String IDD_IPADDRESSES_FIELD = "ip-addresses"; + private static final String IDD_IDENTITY_TYPE_FIELD = "identity-type"; + private InstanceSerializer() {} public static InstanceRegistration registrationFromSlime(Slime slime) { @@ -30,7 +52,7 @@ public class InstanceSerializer { return new InstanceRegistration(requireField(PROVIDER_FIELD, root).asString(), requireField(DOMAIN_FIELD, root).asString(), requireField(SERVICE_FIELD, root).asString(), - requireField(ATTESTATION_DATA_FIELD, root).asString(), + attestationDataToIdentityDocument(StringUtilities.unescape(requireField(ATTESTATION_DATA_FIELD, root).asString())), Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString())); } @@ -51,6 +73,26 @@ public class InstanceSerializer { return slime; } + private static SignedIdentityDocument attestationDataToIdentityDocument(String attestationData) { + Slime slime = SlimeUtils.jsonToSlime(attestationData); + Cursor root = slime.get(); + String signature = requireField(IDD_SIGNATURE_FIELD, root).asString(); + long signingKeyVersion = requireField(IDD_SIGNING_KEY_VERSION_FIELD, root).asLong(); + VespaUniqueInstanceId providerUniqueId = VespaUniqueInstanceId.fromDottedString(requireField(IDD_PROVIDER_UNIQUE_ID_FIELD, root).asString()); + AthenzService athenzService = new AthenzService(requireField(IDD_PROVIDER_SERVICE_FIELD, root).asString()); + long documentVersion = requireField(IDD_DOCUMENT_VERSION_FIELD, root).asLong(); + String configserverHostname = requireField(IDD_CONFIGSERVER_HOSTNAME_FIELD, root).asString(); + String instanceHostname = requireField(IDD_INSTANCE_HOSTNAME_FIELD, root).asString(); + double createdAtTimestamp = requireField(IDD_CREATED_AT_FIELD, root).asDouble(); + Instant createdAt = Instant.ofEpochSecond(Double.valueOf(createdAtTimestamp).longValue()); + Set<String> ips = new HashSet<>(); + requireField(IDD_IPADDRESSES_FIELD, root).traverse((ArrayTraverser) (__, entry) -> ips.add(entry.asString())); + IdentityType identityType = IdentityType.fromId(requireField(IDD_IDENTITY_TYPE_FIELD, root).asString()); + + return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion, + configserverHostname, instanceHostname, createdAt, ips, identityType); + } + private static Cursor requireField(String fieldName, Cursor root) { var field = root.field(fieldName); if (!field.valid()) throw new IllegalArgumentException("Missing required field '" + fieldName + "'"); |