diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProvider.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProvider.java new file mode 100644 index 0000000..0d91522 --- /dev/null +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProvider.java @@ -0,0 +1,699 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.auth.KeyStore; +import com.yahoo.athenz.auth.token.PrincipalToken; +import com.yahoo.athenz.auth.token.Token; +import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; +import com.yahoo.athenz.common.server.dns.HostnameResolver; +import com.yahoo.athenz.common.server.util.ResourceUtils; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import com.yahoo.athenz.instance.provider.ResourceException; +import com.yahoo.athenz.zts.InstanceRegisterToken; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.eclipse.jetty.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import java.security.PrivateKey; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class InstanceServerCertProvider implements InstanceProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(InstanceServerCertProvider.class); + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + private static final String URI_HOSTNAME_PREFIX = "athenz://hostname/"; + + static final String ZTS_PROP_PROVIDER_DNS_SUFFIX = "athenz.zts.provider_dns_suffix"; + static final String ZTS_PROP_PRINCIPAL_LIST = "athenz.zts.provider_service_list"; + static final String ZTS_PROP_EXPIRY_TIME = "athenz.zts.provider_token_expiry_time"; + + static final String ZTS_PROVIDER_SERVICE = "sys.auth.zts"; + + public static final String HDR_KEY_ID = "kid"; + public static final String HDR_TOKEN_TYPE = "typ"; + public static final String HDR_TOKEN_JWT = "jwt"; + + public static final String CLAIM_PROVIDER = "provider"; + public static final String CLAIM_DOMAIN = "domain"; + public static final String CLAIM_SERVICE = "service"; + public static final String CLAIM_CLIENT_ID = "client_id"; + public static final String CLAIM_INSTANCE_ID = "instance_id"; + + KeyStore keyStore = null; + Set dnsSuffixes = null; + String provider = null; + String keyId = null; + PrivateKey key = null; + SignatureAlgorithm keyAlg = null; + Set principals = null; + HostnameResolver hostnameResolver = null; + JwtsSigningKeyResolver signingKeyResolver = null; + int expiryTime; + + @Override + public Scheme getProviderScheme() { + return Scheme.CLASS; + } + + @Override + public void initialize(String provider, String providerEndpoint, SSLContext sslContext, + KeyStore keyStore) { + + // save our provider name + + this.provider = provider; + + // obtain list of valid principals for this principal if + // one is configured + + final String principalList = System.getProperty(ZTS_PROP_PRINCIPAL_LIST); + if (principalList != null && !principalList.isEmpty()) { + principals = new HashSet<>(Arrays.asList(principalList.split(","))); + } + + // determine the dns suffix. if this is not specified we'll just default to zts.athenz.cloud + + dnsSuffixes = new HashSet<>(); + String dnsSuffix = System.getProperty(ZTS_PROP_PROVIDER_DNS_SUFFIX, "zts.athenz.cloud"); + if (StringUtil.isEmpty(dnsSuffix)) { + dnsSuffix = "zts.athenz.cloud"; + } + dnsSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + + this.keyStore = keyStore; + + // get expiry time for any generated tokens - default 30 mins + + final String expiryTimeStr = System.getProperty(ZTS_PROP_EXPIRY_TIME, "30"); + expiryTime = Integer.parseInt(expiryTimeStr); + + // initialize our jwt key resolver + + signingKeyResolver = new JwtsSigningKeyResolver(null, null); + } + + @Override + public void setPrivateKey(PrivateKey key, String keyId, SignatureAlgorithm keyAlg) { + this.key = key; + this.keyId = keyId; + this.keyAlg = keyAlg; + } + + @Override + public void setHostnameResolver(HostnameResolver hostnameResolver) { + this.hostnameResolver = hostnameResolver; + } + + private ResourceException forbiddenError(String message) { + LOGGER.error(message); + return new ResourceException(ResourceException.FORBIDDEN, message); + } + + @Override + public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) { + return validateInstanceRequest(confirmation, true); + } + + @Override + public InstanceConfirmation refreshInstance(InstanceConfirmation confirmation) { + return validateInstanceRequest(confirmation, false); + } + + InstanceConfirmation validateInstanceRequest(InstanceConfirmation confirmation, boolean registerInstance) { + + // we need to validate the token which is our attestation + // data for the service requesting a certificate + + final String instanceDomain = confirmation.getDomain(); + final String instanceService = confirmation.getService(); + + final Map instanceAttributes = confirmation.getAttributes(); + final String csrPublicKey = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY); + + // make sure this service has been configured to be supported + // by this zts provider + + if (principals != null && !principals.contains(instanceDomain + "." + instanceService)) { + throw forbiddenError("Service not supported to be launched by Server Certificate Provider"); + } + + // we're supporting two attestation data models with our provider + // 1) public / private key pair with service tokens - these + // are always starting with v=S1;... string + // 2) provider registration tokens - using jwts + + final String attestationData = confirmation.getAttestationData(); + if (StringUtil.isEmpty(attestationData)) { + throw forbiddenError("Service credentials not provided"); + } + + boolean tokenValidated; + Map attributes; + StringBuilder errMsg = new StringBuilder(256); + if (attestationData.startsWith("v=S1;")) { + + // set our cert attributes in the return object + // for ZTS we do not allow refresh of those certificates + + attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_CERT_REFRESH, "false"); + + tokenValidated = validateServiceToken(attestationData, instanceDomain, + instanceService, csrPublicKey, errMsg); + + } else { + + // for token based request we do support refresh operation + + attributes = Collections.emptyMap(); + + final String instanceId = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_ID); + tokenValidated = validateRegisterToken(attestationData, instanceDomain, + instanceService, instanceId, registerInstance, errMsg); + } + + if (!tokenValidated) { + LOGGER.error(errMsg.toString()); + throw forbiddenError("Unable to validate Certificate Request Auth Token: " + errMsg); + } + + final String clientIp = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_CLIENT_IP); + final String sanIpStr = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_SAN_IP); + final String hostname = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_HOSTNAME); + final String sanUri = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_SAN_URI); + + // validate the IP address if one is provided + + String[] sanIps = null; + if (sanIpStr != null && !sanIpStr.isEmpty()) { + sanIps = sanIpStr.split(","); + } + + if (!validateSanIp(sanIps, clientIp)) { + throw forbiddenError("Unable to validate request IP address"); + } + + // validate the hostname in payload + // IP in clientIP can be NATed. For validating hostname, rely on sanIPs, which come + // from the client, and are already matched with clientIp + + if (!validateHostname(hostname, sanIps)) { + throw forbiddenError("Unable to validate certificate request hostname"); + } + + // validate san URI + if (!validateSanUri(sanUri, hostname)) { + throw forbiddenError("Unable to validate certificate request URI hostname"); + } + + // validate the certificate san DNS names + + StringBuilder instanceId = new StringBuilder(256); + if (!validateCertRequestSanDnsNames(instanceAttributes, instanceDomain, + instanceService, dnsSuffixes, null, null, false, instanceId, null)) { + throw forbiddenError("Unable to validate certificate request DNS"); + } + + confirmation.setAttributes(attributes); + return confirmation; + } + + @Override + public InstanceRegisterToken getInstanceRegisterToken(InstanceConfirmation details) { + + // ZTS Server has already verified that the caller has update + // rights over the given service so we'll just generate + // an instance register token and return to the client + + final String principal = InstanceUtils.getInstanceProperty(details.getAttributes(), + InstanceProvider.ZTS_REQUEST_PRINCIPAL); + final String instanceId = InstanceUtils.getInstanceProperty(details.getAttributes(), + InstanceProvider.ZTS_INSTANCE_ID); + final String tokenId = UUID.randomUUID().toString(); + + // first we'll generate and sign our token + + final String registerToken = Jwts.builder() + .setId(tokenId) + .setSubject(ResourceUtils.serviceResourceName(details.getDomain(), details.getService())) + .setIssuedAt(Date.from(Instant.now())) + .setIssuer(provider) + .setAudience(provider) + .claim(CLAIM_PROVIDER, details.getProvider()) + .claim(CLAIM_DOMAIN, details.getDomain()) + .claim(CLAIM_SERVICE, details.getService()) + .claim(CLAIM_INSTANCE_ID, instanceId) + .claim(CLAIM_CLIENT_ID, principal) + .setHeaderParam(HDR_KEY_ID, keyId) + .setHeaderParam(HDR_TOKEN_TYPE, HDR_TOKEN_JWT) + .signWith(key, keyAlg) + .compact(); + + // finally return our token to the caller + + return new InstanceRegisterToken() + .setProvider(details.getProvider()) + .setDomain(details.getDomain()) + .setService(details.getService()) + .setAttestationData(registerToken); + } + + /** + * verifies that at least one of the sanIps matches clientIp + * @param sanIps an array of SAN IPs + * @param clientIp the client IP address + * @return true if sanIps is null or one of the sanIps matches. false otherwise + */ + boolean validateSanIp(final String[] sanIps, final String clientIp) { + + LOGGER.debug("Validating sanIps: {}, clientIp: {}", sanIps, clientIp); + + // if we have an IP specified in the CSR, one of the sanIp must match our client IP + if (sanIps == null || sanIps.length == 0) { + return true; + } + + if (clientIp == null || clientIp.isEmpty()) { + return false; + } + + // It's possible both ipv4, ipv6 addresses are mentioned in sanIP + for (String sanIp: sanIps) { + if (sanIp.equals(clientIp)) { + return true; + } + } + + LOGGER.error("Unable to match sanIp: {} with clientIp:{}", sanIps, clientIp); + return false; + } + + /** + * returns true if an empty hostname attribute is passed + * returns true if a non-empty hostname attribute is passed and all IPs + * passed in sanIp match the IPs that hostname resolves to. + * returns false in all other cases + * @param hostname host name to check against specified IPs + * @param sanIps list of IPs to check against the specified hostname + * @return true or false + */ + boolean validateHostname(final String hostname, final String[] sanIps) { + + LOGGER.debug("Validating hostname: {}, sanIps: {}", hostname, sanIps); + + if (hostname == null || hostname.isEmpty()) { + LOGGER.info("Request contains no hostname entry for validation"); + // if more than one sanIp is passed, all sanIPs must map to hostname, and hostname is a must + if (sanIps != null && sanIps.length > 1) { + LOGGER.error("SanIps:{} > 1, and hostname is empty", sanIps.length); + return false; + } + return true; + } + + // IP in clientIp can be NATed. Rely on sanIp, which comes from the + // client, and is already matched with clientIp + // sanIp should be non-empty + + if (sanIps == null || sanIps.length == 0) { + LOGGER.error("Request contains no sanIp entry for hostname:{} validation", hostname); + return false; + } + + // All entries in sanIP must be one of the IPs that hostname resolves + + Set hostIps = hostnameResolver.getAllByName(hostname); + for (String sanIp: sanIps) { + if (!hostIps.contains(sanIp)) { + LOGGER.error("One of sanIp: {} is not present in HostIps: {}", hostIps, sanIps); + return false; + } + } + + return true; + } + + /** + * verifies if sanUri contains athenz://hostname/, the value matches the hostname + * @param sanUri the SAN URI that includes athenz hostname + * @param hostname name of the host to check against + * @return true if there is no SAN URI or the hostname is included in it, otherwise false + */ + boolean validateSanUri(final String sanUri, final String hostname) { + + LOGGER.debug("Validating sanUri: {}, hostname: {}", sanUri, hostname); + + if (sanUri == null || sanUri.isEmpty()) { + LOGGER.info("Request contains no sanURI to verify"); + return true; + } + + for (String uri: sanUri.split(",")) { + int idx = uri.indexOf(URI_HOSTNAME_PREFIX); + if (idx != -1) { + if (!uri.substring(idx + URI_HOSTNAME_PREFIX.length()).equals(hostname)) { + LOGGER.error("SanURI: {} does not contain hostname: {}", sanUri, hostname); + return false; + } + } + } + + return true; + } + + boolean validateServiceToken(final String signedToken, final String domainName, + final String serviceName, final String csrPublicKey, StringBuilder errMsg) { + + final PrincipalToken serviceToken = authenticate(signedToken, keyStore, csrPublicKey, errMsg); + if (serviceToken == null) { + return false; + } + + // verify that domain and service name match + + if (!serviceToken.getDomain().equalsIgnoreCase(domainName)) { + errMsg.append("validate failed: domain mismatch: "). + append(serviceToken.getDomain()).append(" vs. ").append(domainName); + return false; + } + + if (!serviceToken.getName().equalsIgnoreCase(serviceName)) { + errMsg.append("validate failed: service mismatch: "). + append(serviceToken.getName()).append(" vs. ").append(serviceName); + return false; + } + + return true; + } + + boolean validateRegisterToken(final String jwToken, final String domainName, final String serviceName, + final String instanceId, boolean registerInstance, StringBuilder errMsg) { + + Jws claims = Jwts.parserBuilder() + .setSigningKeyResolver(signingKeyResolver) + .setAllowedClockSkewSeconds(60) + .build() + .parseClaimsJws(jwToken); + + // verify that token audience is set for our service + + Claims claimsBody = claims.getBody(); + if (!ZTS_PROVIDER_SERVICE.equals(claimsBody.getAudience())) { + errMsg.append("token audience is not ZTS provider: ").append(claimsBody.getAudience()); + return false; + } + + // need to verify that the issue time is not before our expiry + // only for register requests. + + if (registerInstance) { + Date issueDate = claimsBody.getIssuedAt(); + if (issueDate == null || issueDate.getTime() < System.currentTimeMillis() - + TimeUnit.MINUTES.toMillis(expiryTime)) { + errMsg.append("token is already expired, issued at: ").append(issueDate); + return false; + } + } + + // verify provider, domain, service, and instance id values + + if (!domainName.equals(claimsBody.get(CLAIM_DOMAIN, String.class))) { + errMsg.append("invalid domain name in token: ").append(claimsBody.get(CLAIM_DOMAIN, String.class)); + return false; + } + if (!serviceName.equals(claimsBody.get(CLAIM_SERVICE, String.class))) { + errMsg.append("invalid service name in token: ").append(claimsBody.get(CLAIM_SERVICE, String.class)); + return false; + } + if (!instanceId.equals(claimsBody.get(CLAIM_INSTANCE_ID, String.class))) { + errMsg.append("invalid instance id in token: ").append(claimsBody.get(CLAIM_INSTANCE_ID, String.class)); + return false; + } + if (!ZTS_PROVIDER_SERVICE.equals(claimsBody.get(CLAIM_PROVIDER, String.class))) { + errMsg.append("invalid provider name in token: ").append(claimsBody.get(CLAIM_PROVIDER, String.class)); + return false; + } + + return true; + } + + PrincipalToken authenticate(final String signedToken, KeyStore keyStore, + final String csrPublicKey, StringBuilder errMsg) { + + PrincipalToken serviceToken; + try { + serviceToken = new PrincipalToken(signedToken); + } catch (IllegalArgumentException ex) { + errMsg.append("authenticate failed: Invalid token: exc="). + append(ex.getMessage()).append(" : credential="). + append(Token.getUnsignedToken(signedToken)); + LOGGER.error(errMsg.toString()); + return null; + } + + // before authenticating verify that this is not an authorized + // service token + + if (serviceToken.getAuthorizedServices() != null) { + errMsg.append("authenticate failed: authorized service token") + .append(" : credential=").append(Token.getUnsignedToken(signedToken)); + LOGGER.error(errMsg.toString()); + return null; + } + + final String tokenDomain = serviceToken.getDomain().toLowerCase(); + final String tokenName = serviceToken.getName().toLowerCase(); + + // get the public key for this token to validate signature + + final String publicKey = keyStore.getPublicKey(tokenDomain, tokenName, + serviceToken.getKeyId()); + + if (!serviceToken.validate(publicKey, 300, false, errMsg)) { + return null; + } + + // finally we want to make sure the public key in the csr + // matches the public key registered in Athenz + + if (!validatePublicKeys(publicKey, csrPublicKey)) { + errMsg.append("CSR and Athenz public key mismatch"); + LOGGER.error(errMsg.toString()); + return null; + } + + return serviceToken; + } + + public boolean validatePublicKeys(final String athenzPublicKey, final String csrPublicKey) { + + // we are going to remove all whitespace, new lines + // in order to compare the pem encoded keys + + Matcher matcher = WHITESPACE_PATTERN.matcher(athenzPublicKey); + final String normAthenzPublicKey = matcher.replaceAll(""); + + matcher = WHITESPACE_PATTERN.matcher(csrPublicKey); + final String normCsrPublicKey = matcher.replaceAll(""); + + return normAthenzPublicKey.equals(normCsrPublicKey); + } + + /** + * validate the specifies sanDNS entries in the certificate request. If the failedDnsNames + * list is specified, it will be populated with the dns names that failed validation. + * However, if the failure is critical (e.g. we couldn't validate hostname, dns names suffix + * list is not specified), then the method will return false and the failedDnsNames list + * will be empty. + * @param attributes attributes from the certificate request + * @param domain name of the domain + * @param service name of the service + * @param dnsSuffixes list of dns suffixes + * @param k8sDnsSuffixes list of k8s dns suffixes + * @param k8sClusterNames list of k8s cluster names + * @param validateHostname flag to indicate whether we should validate hostname + * @param instanceId instance id value to be returned + * @param failedDnsNames list of failed dns names to be returned + * @return true if all dns names are valid, false otherwise + */ + public static boolean validateCertRequestSanDnsNames(final Map attributes, final String domain, + final String service, final Set dnsSuffixes, final List k8sDnsSuffixes, + final List k8sClusterNames, boolean validateHostname, StringBuilder instanceId, + List failedDnsNames) { + + // make sure we have valid dns suffix specified + + if (dnsSuffixes == null || dnsSuffixes.isEmpty()) { + LOGGER.error("No Cloud Provider DNS suffix specified for validation"); + return false; + } + + // first check to see if we're given any san dns names to validate + // if the list is empty then something is not right thus we'll + // reject the request + + final String hostnames = InstanceUtils.getInstanceProperty(attributes, InstanceProvider.ZTS_INSTANCE_SAN_DNS); + if (StringUtil.isEmpty(hostnames)) { + LOGGER.error("Request contains no SAN DNS entries for validation"); + return false; + } + String[] hosts = hostnames.split(","); + + // extract the instance id from the request + + if (!extractCertRequestInstanceId(attributes, hosts, dnsSuffixes, instanceId)) { + LOGGER.error("Request does not contain expected instance id entry"); + return false; + } + + // for hostnames that are included in the sanDNS entry in the certificate we have + // a couple of requirements: + // a) the sanDNS entry must end with . + // b) one of the prefix components must be the name + + List hostNameSuffixList = new ArrayList<>(); +// final String dashDomain = domain.replace('.', '-'); + + String[] subDomainPortionList = domain.split("\\."); + Collections.reverse(Arrays.asList(subDomainPortionList)); + StringJoiner reversedSubDomain = new StringJoiner("."); + for (String subDomainPortion : subDomainPortionList) { + reversedSubDomain.add(subDomainPortion); + } + for (String dnsSuffix : dnsSuffixes) { + hostNameSuffixList.add("." + reversedSubDomain + "." + dnsSuffix); + } + + // generate our cluster based names if we have clusters configured + + Set clusterNameSet = null; + if (k8sClusterNames != null && !k8sClusterNames.isEmpty()) { + clusterNameSet = new HashSet<>(); + for (String clusterName : k8sClusterNames) { + for (String dnsSuffix : dnsSuffixes) { + clusterNameSet.add(service + "." + reversedSubDomain + "." + clusterName + "." + dnsSuffix); + } + } + } + + // if we have a hostname configured then verify it matches one of formats + + if (validateHostname) { + final String hostname = InstanceUtils.getInstanceProperty(attributes, InstanceProvider.ZTS_INSTANCE_HOSTNAME); + if (!StringUtil.isEmpty(hostname) && !InstanceUtils.validateSanDnsName(hostname, service, hostNameSuffixList, k8sDnsSuffixes, clusterNameSet)) { + LOGGER.error("InstanceUtils.validateSanDnsName() failed with " + + "domain: {}, reversedSubDomain: {}, service: {}, hostname: {}, " + + "hostNameSuffixList: {}, k8sDnsSuffixes: {}, clusterNameSet: {}", + domain, reversedSubDomain, service, hostname, hostNameSuffixList, k8sDnsSuffixes, clusterNameSet); + return false; + } + } + + // validate the entries in our san dns list + + boolean hostCheck = false; + for (String host : hosts) { + + // ignore any entries used for instance id since we've processed + // those already when looking for the instance id + + if (host.contains(InstanceUtils.ZTS_CERT_INSTANCE_ID)) { + continue; + } + + if (!InstanceUtils.validateSanDnsName(host, service, hostNameSuffixList, k8sDnsSuffixes, clusterNameSet)) { + + // if we're not interested in the list of failed hostnames then + // we'll return failure right away. otherwise we'll keep track + // of the failed hostname and continue with the rest of the list + + if (failedDnsNames == null) { + LOGGER.error("InstanceUtils.validateSanDnsName() failed with " + + "domain: {}, reversedSubDomain: {}, service: {}, host: {}, " + + "hostNameSuffixList: {}, k8sDnsSuffixes: {}, clusterNameSet: {}", + domain, reversedSubDomain, service, host, hostNameSuffixList, k8sDnsSuffixes, clusterNameSet); + return false; + } else { + failedDnsNames.add(host); + continue; + } + } + + hostCheck = true; + } + + // if we have no host entry that it's a failure. We're going to + // make sure the failedDnsNames list is empty and return false + // so the caller knows this is a critical failure as opposed to + // failure of not being able to validate the specified entries + + if (!hostCheck) { + LOGGER.error("hostCheck failed with " + + "domain: {}, reversedSubDomain: {}, service: {}, failedDnsNames: {}, " + + "hostNameSuffixList: {}, k8sDnsSuffixes: {}, clusterNameSet: {}", + domain, reversedSubDomain, service, failedDnsNames, hostNameSuffixList, k8sDnsSuffixes, clusterNameSet); + LOGGER.error("Request does not contain expected host SAN DNS entry"); + if (failedDnsNames != null) { + failedDnsNames.clear(); + } + return false; + } + + // if we got here, then we're good to go as long as the + // failedDnsNames list is empty or null. + // if it's not empty then we have some failed entries + // and if it's null, then we would have already returned + // failure when processing the list + + return failedDnsNames == null || failedDnsNames.isEmpty(); + } + + private static boolean extractCertRequestInstanceId(final Map attributes, final String[] hosts, + final Set dnsSuffixes, StringBuilder instanceId) { + + for (String host : hosts) { + + int idx = host.indexOf(InstanceUtils.ZTS_CERT_INSTANCE_ID); + if (idx != -1) { + + // verify that we already don't have an instance id specified + + if (instanceId.length() != 0) { + LOGGER.error("Multiple instance id values specified: {}, {}", host, instanceId); + return false; + } + + if (!dnsSuffixes.contains(host.substring(idx + InstanceUtils.ZTS_CERT_INSTANCE_ID_LEN))) { + LOGGER.error("Host: {} does not have expected instance id format", host); + return false; + } + + instanceId.append(host, 0, idx); + } + } + + // if we found a value from our dns name values then we return right away + // otherwise, we need to look at the uri values to extract the instance id + + if (instanceId.length() != 0) { + return true; + } else { + return InstanceUtils.extractCertRequestUriId(attributes, instanceId); + } + } +} diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceWorkloadIPTokenProvider.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceWorkloadIPTokenProvider.java index a445053..dcf847b 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceWorkloadIPTokenProvider.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceWorkloadIPTokenProvider.java @@ -188,7 +188,7 @@ InstanceConfirmation validateInstanceRequest(InstanceConfirmation confirmation, if (!tokenValidated) { LOGGER.error(errMsg.toString()); - throw forbiddenError("Unable to validate Certificate Request Auth Token"); + throw forbiddenError("Unable to validate Certificate Request Auth Token: " + errMsg); } final String clientIp = InstanceUtils.getInstanceProperty(instanceAttributes, diff --git a/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProviderTest.java b/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProviderTest.java new file mode 100644 index 0000000..b5cbdf3 --- /dev/null +++ b/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceServerCertProviderTest.java @@ -0,0 +1,1060 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.auth.KeyStore; +import com.yahoo.athenz.auth.token.PrincipalToken; +import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.athenz.common.server.dns.HostnameResolver; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import com.yahoo.athenz.instance.provider.ResourceException; +import com.yahoo.athenz.zts.InstanceRegisterToken; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider.*; +import static org.testng.Assert.*; +import static org.testng.Assert.assertEquals; + +public class InstanceServerCertProviderTest { + + private String servicePublicKeyStringK0 = null; + private String servicePrivateKeyStringK0 = null; + + @BeforeMethod + public void setup() throws IOException { + Path path = Paths.get("./src/test/resources/public_k0.key"); + servicePublicKeyStringK0 = new String(Files.readAllBytes(path)); + + path = Paths.get("./src/test/resources/unit_test_private_k0.key"); + servicePrivateKeyStringK0 = new String(Files.readAllBytes(path)); + } + + @Test + public void testInitializeDefaults() { + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + assertTrue(provider.dnsSuffixes.contains("zts.athenz.cloud")); + assertEquals(InstanceProvider.Scheme.CLASS, provider.getProviderScheme()); + assertNull(provider.keyStore); + assertNull(provider.principals); + provider.close(); + } + + @Test + public void testInitialize() { + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PROVIDER_DNS_SUFFIX, "zts.cloud"); + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "athenz.api,sports.backend"); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + assertTrue(provider.dnsSuffixes.contains("zts.cloud")); + assertNull(provider.keyStore); + assertEquals(provider.principals.size(), 2); + assertTrue(provider.principals.contains("athenz.api")); + assertTrue(provider.principals.contains("sports.backend")); + provider.close(); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PROVIDER_DNS_SUFFIX, ""); + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, ""); + + provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + assertTrue(provider.dnsSuffixes.contains("zts.athenz.cloud")); + assertNull(provider.keyStore); + assertNull(provider.principals); + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PROVIDER_DNS_SUFFIX); + } + + @Test + public void testRefreshInstance() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports.football", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.football.api"); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports.football", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports.football"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.football.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + assertNotNull(provider.refreshInstance(confirmation)); + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testValidateSanIp() { + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + assertTrue(provider.validateSanIp(new String[]{"10.1.1.1"}, "10.1.1.1")); + assertTrue(provider.validateSanIp(null, "10.1.1.1")); + assertTrue(provider.validateSanIp(new String[]{}, "10.1.1.1")); + assertFalse(provider.validateSanIp(new String[]{""}, "10.1.1.1")); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.2"}, "10.1.1.1")); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.2"}, null)); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.2"}, "")); + + // ipv6 + assertTrue(provider.validateSanIp(new String[]{"2001:db8:a0b:12f0:0:0:0:1"}, "2001:db8:a0b:12f0:0:0:0:1")); + assertTrue(provider.validateSanIp(null, "2001:db8:a0b:12f0:0:0:0:1")); + assertTrue(provider.validateSanIp(new String[]{}, "2001:db8:a0b:12f0:0:0:0:1")); + assertFalse(provider.validateSanIp(new String[]{"2002:db9:a0b:12f0:0:0:0:1"}, "2001:db8:a0b:12f0:0:0:0:1")); + assertFalse(provider.validateSanIp(new String[]{"2002:db9:a0b:12f0:0:0:0:1"}, "10.1.1.1")); + assertFalse(provider.validateSanIp(new String[]{"2002:db9:a0b:12f0:0:0:0:1"}, null)); + assertFalse(provider.validateSanIp(new String[]{"2002:db9:a0b:12f0:0:0:0:1"}, "")); + + // ipv4 and ipv6 mixed + assertTrue(provider.validateSanIp(new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"}, "10.1.1.1")); + assertTrue(provider.validateSanIp(new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"}, "2001:db8:a0b:12f0:0:0:0:1")); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"}, "10.1.1.2")); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"}, null)); + assertFalse(provider.validateSanIp(new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"}, "")); + + provider.close(); + } + + @Test + public void testValidateHostname() { + HostnameResolver hostnameResolver = Mockito.mock(HostnameResolver.class); + Mockito.when(hostnameResolver.getAllByName("abc.athenz.com")) + .thenReturn(new HashSet<>(Arrays.asList("10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"))); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + provider.setHostnameResolver(hostnameResolver); + + assertTrue(provider.validateHostname("abc.athenz.com", new String[]{"10.1.1.1"})); + assertTrue(provider.validateHostname("abc.athenz.com", new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"})); + assertFalse(provider.validateHostname("abc.athenz.com", new String[]{"10.1.1.2"})); + assertFalse(provider.validateHostname("abc.athenz.com", new String[]{"10.1.1.1", "1:2:3:4:5:6:7:8"})); + assertFalse(provider.validateHostname("abc.athenz.com", new String[]{"10.1.1.2", "1:2:3:4:5:6:7:8"})); + + // If hostname is passed, sanIp must be non empty + assertFalse(provider.validateHostname("abc.athenz.com", null)); + assertFalse(provider.validateHostname("abc.athenz.com", new String[]{})); + assertFalse(provider.validateHostname("abc.athenz.com", new String[]{""})); + + // It's possible client didn't set Hostname payload. One sanIp be optionally set, and would have been matched with clientIp upstream + assertTrue(provider.validateHostname("", new String[]{"10.1.1.1"})); + assertTrue(provider.validateHostname(null, new String[]{"10.1.1.1"})); + + // If more than one sanIp is passed, hostname must be non empty + assertFalse(provider.validateHostname(null, new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"})); + assertFalse(provider.validateHostname("", new String[]{"10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1"})); + + provider.close(); + } + + @Test + public void testValidateSanUri() { + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + assertTrue(provider.validateSanUri("athenz://hostname/abc.athenz.com", "abc.athenz.com")); + assertTrue(provider.validateSanUri("spiffe://movies/sa/writer,athenz://hostname/abc.athenz.com", "abc.athenz.com")); + assertTrue(provider.validateSanUri("spiffe://movies/sa/writer,athenz://hostname/abc.athenz.com,athenz://instanceid/zts/abc.athenz.com", "abc.athenz.com")); + assertTrue(provider.validateSanUri("spiffe://movies/sa/writer,athenz://hostname/abc.athenz.com,athenz://hostname/abc.athenz.com", "abc.athenz.com")); + + assertTrue(provider.validateSanUri("", "abc.athenz.com")); + assertTrue(provider.validateSanUri(null, "abc.athenz.com")); + + assertFalse(provider.validateSanUri("athenz://hostname/abc.athenz.cm", "def.athenz.com")); + assertFalse(provider.validateSanUri("spiffe://movies/sa/writer, athenz://hostname/abc.athenz.cm", "def.athenz.com")); + assertFalse(provider.validateSanUri("spiffe://movies/sa/writer,athenz://hostname/abc.athenz.com,athenz://hostname/def.athenz.com", "abc.athenz.com")); + provider.close(); + } + + @Test + public void testAuthenticate() { + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + StringBuilder errMsg = new StringBuilder(256); + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + String token = "invalidtoken"; + assertNull(provider.authenticate(token, null, servicePublicKeyStringK0, errMsg)); + assertTrue(errMsg.toString().contains("Invalid token")); + + errMsg.setLength(0); + token = "v=S1;d=domain;n=service;t=1234;e=1235;k=0;h=host1;i=1.2.3.4;b=svc1,svc2;s=signature;bk=0;bn=svc1;bs=signature"; + assertNull(provider.authenticate(token, null, servicePublicKeyStringK0, errMsg)); + assertTrue(errMsg.toString().contains("authorized service token")); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + errMsg.setLength(0); + assertNotNull(provider.authenticate(tokenToSign.getSignedToken(), keystore, servicePublicKeyStringK0, errMsg)); + + // test with mismatch public key + + assertNull(provider.authenticate(tokenToSign.getSignedToken(), keystore, "publicKey", errMsg)); + + // create invalid signature + + errMsg.setLength(0); + assertNull(provider.authenticate(tokenToSign.getSignedToken().replace(";s=", ";s=abc"), + keystore, servicePublicKeyStringK0, errMsg)); + provider.close(); + } + + @Test + public void testValidateToken() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + StringBuilder errMsg = new StringBuilder(256); + + String token = "invalidtoken"; + assertFalse(provider.validateServiceToken(token, "sports", "api", servicePublicKeyStringK0, errMsg)); + assertTrue(errMsg.toString().contains("Invalid token")); + + errMsg.setLength(0); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + errMsg.setLength(0); + assertTrue(provider.validateServiceToken(tokenToSign.getSignedToken(), "sports", "api", + servicePublicKeyStringK0, errMsg)); + + errMsg.setLength(0); + assertFalse(provider.validateServiceToken(tokenToSign.getSignedToken(), "sports", "ui", + servicePublicKeyStringK0, errMsg)); + assertTrue(errMsg.toString().contains("service mismatch")); + + errMsg.setLength(0); + assertFalse(provider.validateServiceToken(tokenToSign.getSignedToken(), "weather", "api", + servicePublicKeyStringK0, errMsg)); + assertTrue(errMsg.toString().contains("domain mismatch")); + + provider.close(); + } + + @Test + public void testConfirmInstance() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + assertNotNull(provider.confirmInstance(confirmation)); + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testConfirmInstanceUnsupportedService() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "backend") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("backend"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "backend.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("Service not supported to be launched by Server Certificate Provider")); + } + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testConfirmInstanceValidHostname() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + HostnameResolver hostnameResolver = Mockito.mock(HostnameResolver.class); + Mockito.when(hostnameResolver.isValidHostname("hostabc.athenz.com")).thenReturn(true); + Mockito.when(hostnameResolver.getAllByName("hostabc.athenz.com")).thenReturn( + new HashSet<>(Arrays.asList("10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1")) + ); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.setHostnameResolver(hostnameResolver); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_HOSTNAME, "hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1,2001:db8:a0b:12f0:0:0:0:1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "athenz://instanceid/zts/hostabc.athenz.com,athenz://hostname/hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + assertNotNull(provider.confirmInstance(confirmation)); + provider.close(); + } + + @Test + public void testConfirmInstanceValidHostnameIpv6() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + HostnameResolver hostnameResolver = Mockito.mock(HostnameResolver.class); + Mockito.when(hostnameResolver.isValidHostname("hostabc.athenz.com")).thenReturn(true); + Mockito.when(hostnameResolver.getAllByName("hostabc.athenz.com")).thenReturn( + new HashSet<>(Arrays.asList("10.1.1.1", "2001:db8:a0b:12f0:0:0:0:1")) + ); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.setHostnameResolver(hostnameResolver); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_HOSTNAME, "hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "2001:db8:a0b:12f0:0:0:0:1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1,2001:db8:a0b:12f0:0:0:0:1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "athenz://instanceid/zts/hostabc.athenz.com,athenz://hostname/hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + assertNotNull(provider.confirmInstance(confirmation)); + provider.close(); + } + + @Test + public void testConfirmInstanceUnknownHostname() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + HostnameResolver hostnameResolver = Mockito.mock(HostnameResolver.class); + Mockito.when(hostnameResolver.isValidHostname("hostabc.athenz.com")).thenReturn(true); + Mockito.when(hostnameResolver.getAllByName("hostabc.athenz.com")).thenReturn(new HashSet<>(Collections.singletonList("10.1.1.2"))); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.setHostnameResolver(hostnameResolver); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_HOSTNAME, "hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("validate certificate request hostname")); + } + provider.close(); + } + + @Test + public void testConfirmInstanceInvalidHostnameUri() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + HostnameResolver hostnameResolver = Mockito.mock(HostnameResolver.class); + Mockito.when(hostnameResolver.isValidHostname("hostabc.athenz.com")).thenReturn(true); + Mockito.when(hostnameResolver.getAllByName("hostabc.athenz.com")).thenReturn(new HashSet<>(Collections.singletonList("10.1.1.1"))); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.setHostnameResolver(hostnameResolver); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_HOSTNAME, "hostabc.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "athenz://instanceid/zts/def.athenz.com,athenz://hostname/def.athenz.com"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("validate certificate request URI hostname")); + } + provider.close(); + } + + @Test + public void testConfirmInstanceInvalidIP() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.2"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("validate request IP address")); + } + provider.close(); + } + + @Test + public void testConfirmInstanceInvalidDNSName() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.weather.zts.athenz.cloud,inst1.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CLIENT_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("validate certificate request DNS")); + } + provider.close(); + } + + @Test + public void testConfirmInstanceInvalidToken() { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + + PrincipalToken tokenToSign = new PrincipalToken.Builder("S1", "sports", "api") + .keyId("v0").salt("salt").issueTime(System.currentTimeMillis() / 1000) + .expirationWindow(3600).build(); + tokenToSign.sign(servicePrivateKeyStringK0); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(tokenToSign.getSignedToken().replace(";s=", ";s=abc")); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("validate Certificate Request Auth Token")); + } + provider.close(); + } + + @Test + public void testGetInstanceRegisterToken() throws IOException { + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, null); + + Path path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + final String keyPem = new String(Files.readAllBytes(path)); + + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + provider.setPrivateKey(privateKey, "k0", SignatureAlgorithm.ES256); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + confirmation.setAttributes(attrs); + + InstanceRegisterToken token = provider.getInstanceRegisterToken(confirmation); + assertNotNull(token.getAttestationData()); + provider.close(); + } + + @Test + public void testConfirmInstanceWithRegisterToken() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + // get our private key now + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + provider.setPrivateKey(privateKey, "k0", SignatureAlgorithm.ES256); + + InstanceConfirmation tokenConfirmation = new InstanceConfirmation(); + tokenConfirmation.setDomain("sports"); + tokenConfirmation.setService("api"); + tokenConfirmation.setProvider("sys.auth.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + tokenConfirmation.setAttributes(attrs); + + InstanceRegisterToken token = provider.getInstanceRegisterToken(tokenConfirmation); + + // generate instance confirmation + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(token.getAttestationData()); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,id001.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + attributes.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + confirmation.setAttributes(attributes); + + assertNotNull(provider.confirmInstance(confirmation)); + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testValidateRegisterTokenMismatchFields() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + // get our private key now + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + provider.setPrivateKey(privateKey, "k0", SignatureAlgorithm.ES256); + + InstanceConfirmation tokenConfirmation = new InstanceConfirmation(); + tokenConfirmation.setDomain("sports"); + tokenConfirmation.setService("api"); + tokenConfirmation.setProvider("sys.auth.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + tokenConfirmation.setAttributes(attrs); + + InstanceRegisterToken token = provider.getInstanceRegisterToken(tokenConfirmation); + + // now let's use the validate method for specific cases + + StringBuilder errMsg = new StringBuilder(); + assertFalse(provider.validateRegisterToken(token.getAttestationData(), + "weather", "api", "id001", false, errMsg)); + assertTrue(errMsg.toString().contains("invalid domain name")); + + // next service mismatch + + errMsg.setLength(0); + assertFalse(provider.validateRegisterToken(token.getAttestationData(), + "sports", "backend", "id001", false, errMsg)); + assertTrue(errMsg.toString().contains("invalid service name")); + + // invalid instance id + + errMsg.setLength(0); + assertFalse(provider.validateRegisterToken(token.getAttestationData(), + "sports", "api", "id002", false, errMsg)); + assertTrue(errMsg.toString().contains("invalid instance id")); + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testConfirmInstanceWithRegisterTokenMismatchProvider() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports.football", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.football.api,weather.api,sports.backend"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("athenz.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + // get our private key now + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + provider.setPrivateKey(privateKey, "k0", SignatureAlgorithm.ES256); + + InstanceConfirmation tokenConfirmation = new InstanceConfirmation(); + tokenConfirmation.setDomain("sports.football"); + tokenConfirmation.setService("api"); + tokenConfirmation.setProvider("sys.auth.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + tokenConfirmation.setAttributes(attrs); + + InstanceRegisterToken token = provider.getInstanceRegisterToken(tokenConfirmation); + + // generate instance confirmation + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setAttestationData(token.getAttestationData()); + confirmation.setDomain("sports.football"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.football.zts.athenz.cloud,id001.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + attributes.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + confirmation.setAttributes(attributes); + + // provider mismatch + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.FORBIDDEN); + } + + // calling validation directly should fail as well + + StringBuilder errMsg = new StringBuilder(); + assertFalse(provider.validateRegisterToken(token.getAttestationData(), + "sports.football", "api", "id001", false, errMsg)); + assertTrue(errMsg.toString().contains("token audience is not ZTS provider")); + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testValidateRegisterTokenMismatchProvider() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api,weather.api,sports.backend"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + // get our private key now + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + provider.setPrivateKey(privateKey, "k0", SignatureAlgorithm.ES256); + + InstanceConfirmation tokenConfirmation = new InstanceConfirmation(); + tokenConfirmation.setDomain("sports"); + tokenConfirmation.setService("api"); + tokenConfirmation.setProvider("athenz.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + tokenConfirmation.setAttributes(attrs); + + InstanceRegisterToken token = provider.getInstanceRegisterToken(tokenConfirmation); + + // calling validation directly should fail with invalid provider + + StringBuilder errMsg = new StringBuilder(); + assertFalse(provider.validateRegisterToken(token.getAttestationData(), + "sports", "api", "id001", false, errMsg)); + assertTrue(errMsg.toString().contains("invalid provider name")); + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testConfirmInstanceEmptyCredentials() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + InstanceConfirmation tokenConfirmation = new InstanceConfirmation(); + tokenConfirmation.setDomain("sports"); + tokenConfirmation.setService("api"); + tokenConfirmation.setProvider("sys.auth.zts"); + Map attrs = new HashMap<>(); + attrs.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + tokenConfirmation.setAttributes(attrs); + + // generate instance confirmation + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.zts"); + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.zts.athenz.cloud,id001.instanceid.athenz.zts.athenz.cloud"); + attributes.put(InstanceProvider.ZTS_INSTANCE_CSR_PUBLIC_KEY, servicePublicKeyStringK0); + attributes.put(InstanceProvider.ZTS_INSTANCE_ID, "id001"); + confirmation.setAttributes(attributes); + + // provider mismatch + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.FORBIDDEN); + assertTrue(ex.getMessage().contains("Service credentials not provided")); + } + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testValidateRegisterTokenNullIssueDate() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + + // first generate token with no issue date + + final String registerToken = Jwts.builder() + .setId("001") + .setSubject("sports.api") + .setIssuer("sys.auth.zts") + .setAudience("sys.auth.zts") + .claim(CLAIM_PROVIDER, "sys.auth.zts") + .claim(CLAIM_DOMAIN, "sports") + .claim(CLAIM_SERVICE, "api") + .claim(CLAIM_INSTANCE_ID, "id001") + .claim(CLAIM_CLIENT_ID, "user.athenz") + .setHeaderParam(HDR_KEY_ID, "k0") + .setHeaderParam(HDR_TOKEN_TYPE, HDR_TOKEN_JWT) + .signWith(privateKey, SignatureAlgorithm.ES256) + .compact(); + + + // with register instance enabled, this is going to be reject since + // there is no issue date + + StringBuilder errMsg = new StringBuilder(); + assertFalse(provider.validateRegisterToken(registerToken, + "sports", "api", "id001", true, errMsg)); + assertTrue(errMsg.toString().contains("token is already expired, issued at: null")); + + // with refresh option it's going to be skipped + + errMsg.setLength(0); + assertTrue(provider.validateRegisterToken(registerToken, + "sports", "api", "id001", false, errMsg)); + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } + + @Test + public void testValidateRegisterTokenExpiredIssueDate() throws IOException { + + KeyStore keystore = Mockito.mock(KeyStore.class); + Mockito.when(keystore.getPublicKey("sports", "api", "v0")).thenReturn(servicePublicKeyStringK0); + + System.setProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST, "sports.api"); + + // get our ec public key + + Path path = Paths.get("./src/test/resources/unit_test_ec_public.key"); + String keyPem = new String(Files.readAllBytes(path)); + PublicKey publicKey = Crypto.loadPublicKey(keyPem); + + InstanceServerCertProvider provider = new InstanceServerCertProvider(); + provider.initialize("sys.auth.zts", "com.yahoo.athenz.instance.provider.impl.InstanceServerCertProvider", null, keystore); + provider.signingKeyResolver.addPublicKey("k0", publicKey); + + path = Paths.get("./src/test/resources/unit_test_ec_private.key"); + keyPem = new String(Files.readAllBytes(path)); + PrivateKey privateKey = Crypto.loadPrivateKey(keyPem); + + // first generate token with no issue date + + Instant issueTime = Instant.ofEpochMilli(System.currentTimeMillis() - + TimeUnit.MINUTES.toMillis(31)); + Date issueDate = Date.from(issueTime); + + final String registerToken = Jwts.builder() + .setId("001") + .setSubject("sports.api") + .setIssuedAt(issueDate) + .setIssuer("sys.auth.zts") + .setAudience("sys.auth.zts") + .claim(CLAIM_PROVIDER, "sys.auth.zts") + .claim(CLAIM_DOMAIN, "sports") + .claim(CLAIM_SERVICE, "api") + .claim(CLAIM_INSTANCE_ID, "id001") + .claim(CLAIM_CLIENT_ID, "user.athenz") + .setHeaderParam(HDR_KEY_ID, "k0") + .setHeaderParam(HDR_TOKEN_TYPE, HDR_TOKEN_JWT) + .signWith(privateKey, SignatureAlgorithm.ES256) + .compact(); + + + // with register instance enabled, this is going to be reject since + // there is no issue date + + StringBuilder errMsg = new StringBuilder(); + assertFalse(provider.validateRegisterToken(registerToken, + "sports", "api", "id001", true, errMsg)); + assertTrue(errMsg.toString().contains("token is already expired, issued at: " + issueDate)); + + // with refresh option it's going to be skipped + + errMsg.setLength(0); + assertTrue(provider.validateRegisterToken(registerToken, + "sports", "api", "id001", false, errMsg)); + + provider.close(); + System.clearProperty(InstanceServerCertProvider.ZTS_PROP_PRINCIPAL_LIST); + } +}