diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java index 7b6e574ceab..be740911aa7 100644 --- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java +++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java @@ -114,7 +114,7 @@ long parseDateValue(Date date) { void setTokenFields() { - setVersion(JwtsHelper.getIntegerClaim(claimsSet, CLAIM_VERSION)); + setVersion(JwtsHelper.getIntegerClaim(claimsSet, CLAIM_VERSION, 0)); List audiences = claimsSet.getAudience(); if (audiences != null && !audiences.isEmpty()) { setAudience(audiences.get(0)); @@ -122,7 +122,7 @@ void setTokenFields() { setExpiryTime(parseDateValue(claimsSet.getExpirationTime())); setIssueTime(parseDateValue(claimsSet.getIssueTime())); setNotBeforeTime(parseDateValue(claimsSet.getNotBeforeTime())); - setAuthTime(JwtsHelper.getLongClaim(claimsSet, CLAIM_AUTH_TIME)); + setAuthTime(JwtsHelper.getLongClaim(claimsSet, CLAIM_AUTH_TIME, 0)); setIssuer(claimsSet.getIssuer()); setSubject(claimsSet.getSubject()); setJwtId(claimsSet.getJWTID()); diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/jwts/JwtsHelper.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/jwts/JwtsHelper.java index 2161c810c1f..469b01f6dfe 100644 --- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/jwts/JwtsHelper.java +++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/jwts/JwtsHelper.java @@ -218,19 +218,21 @@ public static ConfigurableJWTProcessor getJWTProcessor(JwtsSign return jwtProcessor; } - public static int getIntegerClaim(JWTClaimsSet claims, final String claim) { + public static int getIntegerClaim(JWTClaimsSet claims, final String claim, int defaultValue) { try { - return claims.getIntegerClaim(claim); + Integer value = claims.getIntegerClaim(claim); + return value == null ? defaultValue : value; } catch (ParseException ex) { - return 0; + return defaultValue; } } - public static long getLongClaim(JWTClaimsSet claims, final String claim) { + public static long getLongClaim(JWTClaimsSet claims, final String claim, long defaultValue) { try { - return claims.getLongClaim(claim); + Long value = claims.getLongClaim(claim); + return value == null ? defaultValue : value; } catch (ParseException ex) { - return 0; + return defaultValue; } } diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java index 0ac33eb2e85..e2b2cb05c66 100644 --- a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java +++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java @@ -310,8 +310,8 @@ public void testParseFailures() { .build(); assertNull(JwtsHelper.getStringClaim(claimsSet, "string")); assertNull(JwtsHelper.getStringListClaim(claimsSet, "stringlist")); - assertEquals(JwtsHelper.getIntegerClaim(claimsSet, "integer"), 0); - assertEquals(JwtsHelper.getLongClaim(claimsSet, "long"), 0); + assertEquals(JwtsHelper.getIntegerClaim(claimsSet, "integer", 0), 0); + assertEquals(JwtsHelper.getLongClaim(claimsSet, "long", -1), -1); assertNull(JwtsHelper.getAudience(claimsSet)); } } diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProvider.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProvider.java new file mode 100644 index 00000000000..560ca66a87c --- /dev/null +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProvider.java @@ -0,0 +1,357 @@ +/* + * 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.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.KeyStore; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.auth.token.jwts.JwtsHelper; +import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigLong; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import com.yahoo.athenz.instance.provider.ResourceException; +import org.eclipse.jetty.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; + +/** + * Instance provider for BuildKite, based on its OIDC setup: + * + */ +public class InstanceBuildKiteProvider implements InstanceProvider { + + + private static final Logger LOGGER = LoggerFactory.getLogger(InstanceBuildKiteProvider.class); + + private static final String URI_INSTANCE_ID_PREFIX = "athenz://instanceid/"; + private static final String URI_SPIFFE_PREFIX = "spiffe://"; + + static final String BUILD_KITE_PROP_PROVIDER_DNS_SUFFIX = "athenz.zts.build_kite.provider_dns_suffix"; + static final String BUILD_KITE_PROP_BOOT_TIME_OFFSET = "athenz.zts.build_kite.boot_time_offset"; + static final String BUILD_KITE_PROP_CERT_EXPIRY_TIME = "athenz.zts.build_kite.cert_expiry_time"; + static final String BUILD_KITE_PROP_AUDIENCE = "athenz.zts.build_kite.audience"; + static final String BUILD_KITE_PROP_ISSUER = "athenz.zts.build_kite.issuer"; + static final String BUILD_KITE_PROP_JWKS_URI = "athenz.zts.build_kite.jwks_uri"; + + static final String BUILD_KITE_ISSUER = "https://agent.buildkite.com"; + static final String BUILD_KITE_ISSUER_JWKS_URI = "https://agent.buildkite.com/.well-known/jwks"; + + public static final String CLAIM_ORGANIZATION_SLUG = "organization_slug"; + public static final String CLAIM_PIPELINE_SLUG = "pipeline_slug"; + public static final String CLAIM_BUILD_NUMBER = "build_number"; + public static final String CLAIM_JOB_ID = "job_id"; + + Set dnsSuffixes = null; + String buildKiteIssuer = null; + String provider = null; + String audience = null; + JwtsSigningKeyResolver signingKeyResolver = null; + Authorizer authorizer = null; + DynamicConfigLong bootTimeOffsetSeconds; + long certExpiryTime; + + @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; + + // lookup the zts audience. if not specified we'll default to athenz.io + + audience = System.getProperty(BUILD_KITE_PROP_AUDIENCE, "athenz.io"); + + // determine the dns suffix. if this is not specified we'll just default to build-kite.athenz.cloud + + final String dnsSuffix = System.getProperty(BUILD_KITE_PROP_PROVIDER_DNS_SUFFIX, "build-kite.athenz.io"); + dnsSuffixes = Set.of(dnsSuffix.split(",")); + + // how long the instance must be booted in the past before we + // stop validating the instance requests + + long timeout = TimeUnit.SECONDS.convert(5, TimeUnit.MINUTES); + bootTimeOffsetSeconds = new DynamicConfigLong(CONFIG_MANAGER, BUILD_KITE_PROP_BOOT_TIME_OFFSET, timeout); + + // get default/max expiry time for any generated tokens - 6 hours + + certExpiryTime = Long.parseLong(System.getProperty(BUILD_KITE_PROP_CERT_EXPIRY_TIME, "360")); + + // initialize our jwt key resolver + + buildKiteIssuer = System.getProperty(BUILD_KITE_PROP_ISSUER, BUILD_KITE_ISSUER); + signingKeyResolver = new JwtsSigningKeyResolver(extractIssuerJwksUri(buildKiteIssuer), null); + } + + String extractIssuerJwksUri(final String issuer) { + + // if we have the value configured then that's what we're going to use + + String jwksUri = System.getProperty(BUILD_KITE_PROP_JWKS_URI); + if (!StringUtil.isEmpty(jwksUri)) { + return jwksUri; + } + + // otherwise we'll assume the issuer follows the standard and + // includes the jwks uri in its openid configuration + + final String openIdConfigUri = issuer + "/.well-known/openid-configuration"; + JwtsHelper helper = new JwtsHelper(); + jwksUri = helper.extractJwksUri(openIdConfigUri, null); + + // if we still don't have a value we'll just return the default value + + return StringUtil.isEmpty(jwksUri) ? BUILD_KITE_ISSUER_JWKS_URI : jwksUri; + } + + private ResourceException forbiddenError(String message) { + LOGGER.error(message); + return new ResourceException(ResourceException.FORBIDDEN, message); + } + + @Override + public void setAuthorizer(Authorizer authorizer) { + this.authorizer = authorizer; + } + + @Override + public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) { + + // before running any checks make sure we have a valid authorizer + + if (authorizer == null) { + throw forbiddenError("Authorizer not available"); + } + + final String instanceDomain = confirmation.getDomain(); + final String instanceService = confirmation.getService(); + final Map instanceAttributes = confirmation.getAttributes(); + + // our request must not have any sanIPs or hostnames + + if (!StringUtil.isEmpty(InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_SAN_IP))) { + throw forbiddenError("Request must not have any sanIP addresses"); + } + + if (!StringUtil.isEmpty(InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_HOSTNAME))) { + throw forbiddenError("Request must not have any hostname values"); + } + + // validate san URI + + if (!validateSanUri(InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_SAN_URI))) { + throw forbiddenError("Unable to validate certificate request URI values"); + } + + // we need to validate the token which is our attestation + // data for the service requesting a certificate + + final String attestationData = confirmation.getAttestationData(); + if (StringUtil.isEmpty(attestationData)) { + throw forbiddenError("Service credentials not provided"); + } + + StringBuilder errMsg = new StringBuilder(256); + final String reqInstanceId = InstanceUtils.getInstanceProperty(instanceAttributes, + InstanceProvider.ZTS_INSTANCE_ID); + if (!validateOIDCToken(attestationData, instanceDomain, instanceService, reqInstanceId, errMsg)) { + throw forbiddenError("Unable to validate Certificate Request: " + errMsg); + } + + // validate the certificate san DNS names + + StringBuilder instanceId = new StringBuilder(256); + if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, instanceDomain, + instanceService, dnsSuffixes, null, null, false, instanceId, null)) { + throw forbiddenError("Unable to validate certificate request sanDNS entries"); + } + + // set our cert attributes in the return object. + // for BuildKite we do not allow refresh of those certificates, and + // the issued certificate can only be used by clients and not servers + + Map attributes = new HashMap<>(); + attributes.put(InstanceProvider.ZTS_CERT_REFRESH, "false"); + attributes.put(InstanceProvider.ZTS_CERT_USAGE, ZTS_CERT_USAGE_CLIENT); + attributes.put(InstanceProvider.ZTS_CERT_EXPIRY_TIME, Long.toString(certExpiryTime)); + + confirmation.setAttributes(attributes); + return confirmation; + } + + @Override + public InstanceConfirmation refreshInstance(InstanceConfirmation confirmation) { + + // we do not allow refresh of BuildKite certificates + + throw forbiddenError("BuildKite X.509 Certificates cannot be refreshed"); + } + + /** + * verifies that sanUri only contains the spiffe and instance id uris + * @param sanUri the SAN URI value + * @return true if it only contains spiffe and instance id uris, otherwise false + */ + boolean validateSanUri(final String sanUri) { + + if (StringUtil.isEmpty(sanUri)) { + LOGGER.debug("Request contains no sanURI to verify"); + return true; + } + + for (String uri: sanUri.split(",")) { + if (uri.startsWith(URI_SPIFFE_PREFIX) || uri.startsWith(URI_INSTANCE_ID_PREFIX)) { + continue; + } + LOGGER.error("Request contains unsupported uri value: {}", uri); + return false; + } + + return true; + } + + boolean validateOIDCToken(final String jwToken, final String domainName, final String serviceName, + final String instanceId, StringBuilder errMsg) { + + JWTClaimsSet claimsSet; + try { + ConfigurableJWTProcessor jwtProcessor = JwtsHelper.getJWTProcessor(signingKeyResolver); + claimsSet = jwtProcessor.process(jwToken, null); + } catch (Exception ex) { + errMsg.append("Unable to parse and validate token: ").append(ex.getMessage()); + return false; + } + + // verify the issuer in set to BuildKite + + if (!buildKiteIssuer.equals(claimsSet.getIssuer())) { + errMsg.append("token issuer is not BuildKite: ").append(claimsSet.getIssuer()); + return false; + } + + // verify that token audience is set for our service + + if (!audience.equals(JwtsHelper.getAudience(claimsSet))) { + errMsg.append("token audience is not ZTS Server audience: ").append(JwtsHelper.getAudience(claimsSet)); + return false; + } + + // need to verify that the issue time is within our configured bootstrap time + + Date issueDate = claimsSet.getIssueTime(); + if (issueDate == null || issueDate.getTime() < System.currentTimeMillis() - + TimeUnit.SECONDS.toMillis(bootTimeOffsetSeconds.get())) { + errMsg.append("job start time is not recent enough, issued at: ").append(issueDate); + return false; + } + + // verify that the instance id matches the repository and run id in the token + + if (!validateInstanceId(instanceId, claimsSet, errMsg)) { + return false; + } + + // verify the domain and service names in the token based on our configuration + + return validateTenantDomainToken(claimsSet, domainName, serviceName, errMsg); + } + + boolean validateInstanceId(final String instanceId, final JWTClaimsSet claimsSet, StringBuilder errMsg) { + + // the format for our instance id is ::: + // https://buildkite.com/docs/apis/rest-api/jobs + + final String organizationSlug = JwtsHelper.getStringClaim(claimsSet, CLAIM_ORGANIZATION_SLUG); + if (StringUtil.isEmpty(organizationSlug)) { + errMsg.append("token does not contain required " + CLAIM_ORGANIZATION_SLUG + " claim"); + return false; + } + final String pipelineSlug = JwtsHelper.getStringClaim(claimsSet, CLAIM_PIPELINE_SLUG); + if (StringUtil.isEmpty(pipelineSlug)) { + errMsg.append("token does not contain required " + CLAIM_PIPELINE_SLUG + " claim"); + return false; + } + final long buildNumber = JwtsHelper.getLongClaim(claimsSet, CLAIM_BUILD_NUMBER, -1); + if (buildNumber == -1) { + errMsg.append("token does not contain required " + CLAIM_BUILD_NUMBER + " claim"); + return false; + } + final String jobId = JwtsHelper.getStringClaim(claimsSet, CLAIM_JOB_ID); + if (StringUtil.isEmpty(jobId)) { + errMsg.append("token does not contain required " + CLAIM_JOB_ID + " claim"); + return false; + } + + final String tokenInstanceId = organizationSlug + ":" + pipelineSlug + ":" + buildNumber + ":" + jobId; + if (!tokenInstanceId.equals(instanceId)) { + errMsg.append("invalid instance id: ").append(tokenInstanceId).append("/").append(instanceId); + return false; + } + return true; + } + + boolean validateTenantDomainToken(final JWTClaimsSet claimsSet, final String domainName, final String serviceName, + StringBuilder errMsg) { + + // we need to generate our resource value based on the subject, which is org:pipeline:ref:commit:step + + final String subject = claimsSet.getSubject(); + if (StringUtil.isEmpty(subject)) { + errMsg.append("token does not contain required subject claim"); + return false; + } + + // generate our principal object and carry out authorization check + + final String resource = domainName + ":" + subject; + Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); + + // BuildKite has no event/action type; instead, the user must allow the push service only for the + // main branch (non-PR), e.g., using 'organization::pipeline::ref:refs/heads/
:*' + // vs 'organization::pipeline::*' for PRs. + + final String action = "build-kite.build"; + if (!authorizer.access(action, resource, principal, null)) { + errMsg.append("authorization check failed for action ").append(action).append(", resource: ").append(resource); + return false; + } + return true; + } +} diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java index b724b37d286..9f46f7bb903 100644 --- a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java @@ -24,13 +24,10 @@ import com.yahoo.athenz.auth.impl.SimplePrincipal; import com.yahoo.athenz.auth.token.jwts.JwtsHelper; import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; -import com.yahoo.athenz.common.server.http.HttpDriver; import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigLong; import com.yahoo.athenz.instance.provider.InstanceConfirmation; import com.yahoo.athenz.instance.provider.InstanceProvider; import com.yahoo.athenz.instance.provider.ResourceException; -import com.yahoo.rdl.JSON; -import com.yahoo.rdl.Struct; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,10 +114,6 @@ public void initialize(String provider, String providerEndpoint, SSLContext sslC signingKeyResolver = new JwtsSigningKeyResolver(extractGitHubIssuerJwksUri(githubIssuer), null); } - HttpDriver getHttpDriver(String url) { - return new HttpDriver.Builder(url, null).build(); - } - String extractGitHubIssuerJwksUri(final String issuer) { // if we have the value configured then that's what we're going to use @@ -133,17 +126,9 @@ String extractGitHubIssuerJwksUri(final String issuer) { // otherwise we'll assume the issuer follows the standard and // includes the jwks uri in its openid configuration - try (HttpDriver httpDriver = getHttpDriver(issuer)) { - String openIdConfig = httpDriver.doGet("/.well-known/openid-configuration", null); - if (!StringUtil.isEmpty(openIdConfig)) { - Struct openIdConfigStruct = JSON.fromString(openIdConfig, Struct.class); - if (openIdConfigStruct != null) { - jwksUri = openIdConfigStruct.getString("jwks_uri"); - } - } - } catch (Exception ex) { - LOGGER.error("Unable to retrieve openid configuration from issuer: {}", issuer, ex); - } + final String openIdConfigUri = issuer + "/.well-known/openid-configuration"; + JwtsHelper helper = new JwtsHelper(); + jwksUri = helper.extractJwksUri(openIdConfigUri, null); // if we still don't have a value we'll just return the default value diff --git a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProviderTest.java b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProviderTest.java new file mode 100644 index 00000000000..12975e9e219 --- /dev/null +++ b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceBuildKiteProviderTest.java @@ -0,0 +1,579 @@ +/* + * 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.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import com.yahoo.athenz.instance.provider.ResourceException; +import org.mockito.Mockito; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class InstanceBuildKiteProviderTest { + + private final File ecPrivateKey = new File("./src/test/resources/unit_test_ec_private.key"); + + private final ClassLoader classLoader = this.getClass().getClassLoader(); + + private final Map testClaims = Map.of( + "iss", "https://agent.buildkite.com", + "aud", "https://athenz.io", + "sub", "organization:my-org:pipeline:my-pipe:ref:refs/heads/main:commit:deadbeef:step:my-step", + "organization_slug", "my-org", + "pipeline_slug", "my-pipe", + "build_number", 123, + "job_id", "job-uuid" + ); + + static void createOpenIdConfigFile(File configFile, File jwksUri) throws IOException { + + final String fileContents = "{\n" + + " \"jwks_uri\": \"file://" + jwksUri.getCanonicalPath() + "\"\n" + + "}"; + Files.createDirectories(configFile.toPath().getParent()); + Files.write(configFile.toPath(), fileContents.getBytes()); + } + + @AfterMethod + public void tearDown() { + System.clearProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE); + System.clearProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI); + System.clearProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_ISSUER); + } + + @Test + public void testInitializeWithConfig() { + String jwksUri = "https://test.jwks"; + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + assertEquals(provider.getProviderScheme(), InstanceProvider.Scheme.CLASS); + } + + @Test + public void testInitializeWithOpenIdConfig() throws IOException { + + File configFile = new File("./src/test/resources/config-openid/.well-known/openid-configuration"); + File jwksUriFile = new File("./src/test/resources/jwt-jwks.json"); + createOpenIdConfigFile(configFile, jwksUriFile); + + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_ISSUER, "file://" + configFile.getCanonicalPath()); + + // std test where the http driver will return null for the config object + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + assertNotNull(provider); + Files.delete(configFile.toPath()); + } + + @Test + public void testConfirmInstance() { + + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + Principal mainPrincipal = SimplePrincipal.create("sports", "api", (String) null); + Principal prPrincipal = SimplePrincipal.create("sports", "pr", (String) null); + String mainResource = "sports:organization:my-org:pipeline:my-pipe:ref:refs/heads/main"; + String prResource = "sports:organization:my-org:pipeline:my-pipe"; + String action = "build-kite.build"; + Mockito.when(authorizer.access(eq(action), startsWith(mainResource), eq(mainPrincipal), isNull())) + .thenReturn(true); + Mockito.when(authorizer.access(eq(action), startsWith(prResource), eq(prPrincipal), isNull())) + .thenReturn(true); + provider.setAuthorizer(authorizer); + + assertTrue(authorizer.access(action, mainResource, mainPrincipal, null)); + assertTrue(authorizer.access(action, mainResource, prPrincipal, null)); + assertFalse(authorizer.access(action, prResource, mainPrincipal, null)); + assertTrue(authorizer.access(action, prResource, prPrincipal, null)); + + // first, test for a build run from the main branch, requesting the main service + + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_ID, "my-org:my-pipe:123:job-uuid"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "spiffe://ns/default/sports/api,athenz://instanceid/sys.auth.build-kite/my-org:my-pipe:job-uuid:123"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "api.sports.build-kite.athenz.io"); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + + Map claims = new HashMap<>(testClaims); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, claims)); + confirmation.setAttributes(instanceAttributes); + + InstanceConfirmation confirmResponse = provider.confirmInstance(confirmation); + assertNotNull(confirmResponse); + assertEquals(confirmResponse.getAttributes().get(InstanceProvider.ZTS_CERT_REFRESH), "false"); + assertEquals(confirmResponse.getAttributes().get(InstanceProvider.ZTS_CERT_USAGE), "client"); + assertEquals(confirmResponse.getAttributes().get(InstanceProvider.ZTS_CERT_EXPIRY_TIME), "360"); + + // next, test for a build from a different branch, requesting the main service + + claims.put("sub", "organization:my-org:pipeline:my-pipe:ref:refs/heads/patch-1:commit:deadbeef:step:my-step"); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, claims)); + confirmation.setAttributes(instanceAttributes); + + try { + provider.confirmInstance(confirmation); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("authorization check failed for action")); + } + + // then, test for a build from a different branch, requesting the pr service + + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "spiffe://ns/default/sports/pr,athenz://instanceid/sys.auth.build-kite/my-org:my-pipe:job-uuid:123"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "pr.sports.build-kite.athenz.io"); + confirmation.setService("pr"); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, claims)); + confirmation.setAttributes(instanceAttributes); + + confirmResponse = provider.confirmInstance(confirmation); + assertNotNull(confirmResponse); + + // finally, test for a build from the main branch, requesting the pr service (not really an intended use case) + + claims.put("sub", "organization:my-org:pipeline:my-pipe:ref:refs/heads/main:commit:deadbeef:step:my-step"); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, claims)); + confirmation.setAttributes(instanceAttributes); + + confirmResponse = provider.confirmInstance(confirmation); + assertNotNull(confirmResponse); + } + + @Test + public void testConfirmInstanceFailures() { + + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks_empty.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + Principal principal = SimplePrincipal.create("sports", "api", (String) null); + Mockito.when(authorizer.access(eq("build-kite.build"), startsWith("sports:organization:my-org:pipeline:my-pipe:ref:refs/heads/main"), eq(principal), isNull())) + .thenReturn(true); + provider.setAuthorizer(authorizer); + + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_ID, "my-org:my-pipe:123:job-uuid"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "spiffe://ns/default/sports/api,athenz://instanceid/sys.auth.build-kite/my-org:my-pipe:job-uuid:123"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "host1.athenz.io"); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, testClaims)); + confirmation.setAttributes(instanceAttributes); + + // without the public key we should get a token validation failure + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("no matching key(s) found")); + } + + provider.close(); + } + + @Test + public void testConfirmInstanceFailuresInvalidSanDNS() { + + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + Principal principal = SimplePrincipal.create("sports", "api", (String) null); + Mockito.when(authorizer.access(eq("build-kite.build"), startsWith("sports:organization:my-org:pipeline:my-pipe:ref:refs/heads/main"), eq(principal), isNull())) + .thenReturn(true); + provider.setAuthorizer(authorizer); + + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_ID, "my-org:my-pipe:123:job-uuid"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "spiffe://ns/default/sports/api,athenz://instanceid/sys.auth.build-kite/my-org:my-pipe:job-uuid:123"); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_DNS, "host1.athenz.io"); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setAttestationData(generateIdToken(Duration.ZERO, testClaims)); + confirmation.setAttributes(instanceAttributes); + + // we should get a failure due to invalid san dns entry + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Unable to validate certificate request sanDNS entries")); + } + } + + @Test + public void testConfirmInstanceWithoutAuthorizer() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + provider.setAuthorizer(null); + try { + provider.confirmInstance(null); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Authorizer not available")); + } + } + + @Test + public void testConfirmInstanceWithSanIP() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_IP, "10.1.1.1"); + confirmation.setAttributes(instanceAttributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Request must not have any sanIP addresses")); + } + } + + @Test + public void testConfirmInstanceWithHostname() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_HOSTNAME, "host1.athenz.io"); + confirmation.setAttributes(instanceAttributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Request must not have any hostname values")); + } + } + + @Test + public void testConfirmInstanceWithSanURI() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + Map instanceAttributes = new HashMap<>(); + instanceAttributes.put(InstanceProvider.ZTS_INSTANCE_SAN_URI, "spiffe://ns/athenz.production/instanceid,https://athenz.io"); + confirmation.setAttributes(instanceAttributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Unable to validate certificate request URI values")); + } + } + + @Test + public void testConfirmInstanceWithoutAttestationData() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Service credentials not provided")); + } + } + + @Test + public void testRefreshNotSupported() { + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, "https://config.athenz.io"); + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + try { + provider.refreshInstance(null); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("BuildKite X.509 Certificates cannot be refreshed")); + } + } + + @Test + public void testValidateSanUri() { + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + assertTrue(provider.validateSanUri(null)); + assertTrue(provider.validateSanUri("")); + assertTrue(provider.validateSanUri("spiffe://ns/athenz.production/instanceid")); + assertTrue(provider.validateSanUri("athenz://instanceid/athenz.production/instanceid")); + assertTrue(provider.validateSanUri("athenz://instanceid/athenz.production/instanceid,spiffe://ns/athenz.production/instanceid")); + assertFalse(provider.validateSanUri("athenz://instanceid/athenz.production/instanceid,spiffe://ns/athenz.production/instanceid,https://athenz.io")); + assertFalse(provider.validateSanUri("athenz://hostname/host1,athenz://instanceid/athenz.production/instanceid")); + assertFalse(provider.validateSanUri("athenz://hostname/host1")); + } + + @Test + public void testValidateOIDCTokenIssuerMismatch() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + // our issuer will not match + + Map claims = new HashMap<>(testClaims); + claims.put("iss", "https://some-other-issuer.com"); + String idToken = generateIdToken(Duration.ZERO, claims); + StringBuilder errMsg = new StringBuilder(256); + boolean result = provider.validateOIDCToken(idToken, "sports", "api", "my-org:my-pipe:123:job-uuid", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token issuer is not BuildKite")); + } + + @Test + public void testValidateOIDCTokenAudienceMismatch() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://test.athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + // our audience will not match + + Map claims = new HashMap<>(testClaims); + claims.put("aud", "https://some-other-audience.com"); + String idToken = generateIdToken(Duration.ZERO, claims); + StringBuilder errMsg = new StringBuilder(256); + boolean result = provider.validateOIDCToken(idToken, "sports", "api", "my-org:my-pipe:123:job-uuid", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token audience is not ZTS Server audience")); + } + + @Test + public void testValidateOIDCTokenStartNotRecentEnough() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + // our issue time is not recent enough + + String idToken = generateIdToken(Duration.ofSeconds(400).negated(), testClaims); + StringBuilder errMsg = new StringBuilder(256); + boolean result = provider.validateOIDCToken(idToken, "sports", "api", "my-org:my-pipe:123:job-uuid", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("job start time is not recent enough")); + } + + @Test + public void testValidateOIDCTokenRunIdMismatch() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + // instance ID from confirmation attributes does not match claims + + String idToken = generateIdToken(Duration.ZERO, testClaims); + StringBuilder errMsg = new StringBuilder(256); + boolean result = provider.validateOIDCToken(idToken, "sports", "api", "my-org:my-pipe:124:job-uuid", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("invalid instance id: my-org:my-pipe:123:job-uuid/my-org:my-pipe:124:job-uuid")); + + // missing organization slug + + Map claims = new HashMap<>(testClaims); + claims.remove("organization_slug"); + idToken = generateIdToken(Duration.ZERO, claims); + errMsg.setLength(0); + result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token does not contain required organization_slug claim")); + + // missing pipeline slug + + claims = new HashMap<>(testClaims); + claims.remove("pipeline_slug"); + idToken = generateIdToken(Duration.ZERO, claims); + errMsg.setLength(0); + result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token does not contain required pipeline_slug claim")); + + // missing job id + + claims = new HashMap<>(testClaims); + claims.remove("job_id"); + idToken = generateIdToken(Duration.ZERO, claims); + errMsg.setLength(0); + result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token does not contain required job_id claim")); + + // missing build number + + claims = new HashMap<>(testClaims); + claims.remove("build_number"); + idToken = generateIdToken(Duration.ZERO, claims); + assertNotNull(idToken); + errMsg.setLength(0); + result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token does not contain required build_number claim")); + } + + @Test + public void testValidateOIDCTokenMissingSubject() { + + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_JWKS_URI, jwksUri); + System.setProperty(InstanceBuildKiteProvider.BUILD_KITE_PROP_AUDIENCE, "https://athenz.io"); + + InstanceBuildKiteProvider provider = new InstanceBuildKiteProvider(); + provider.initialize("sys.auth.build_kite", + "class://com.yahoo.athenz.instance.provider.impl.InstanceBuildKiteProvider", null, null); + + // create an id token without the subject claim + + Map claims = new HashMap<>(testClaims); + claims.remove("sub"); + String idToken = generateIdToken(Duration.ZERO, claims); + + StringBuilder errMsg = new StringBuilder(256); + boolean result = provider.validateOIDCToken(idToken, "sports", "api", "my-org:my-pipe:123:job-uuid", errMsg); + assertFalse(result); + assertTrue(errMsg.toString().contains("token does not contain required subject claim")); + } + + private String generateIdToken(Duration issuedAtOffset, Map claims) { + + try { + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey); + JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder() + .expirationTime(Date.from(Instant.now().plus(issuedAtOffset).plusSeconds(3600))) + .issueTime(Date.from(Instant.now().plus(issuedAtOffset))); + claims.forEach(claimsSetBuilder::claim); + + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), + claimsSetBuilder.build()); + signedJWT.sign(signer); + return signedJWT.serialize(); + } catch (Exception ex) { + return null; + } + } +} diff --git a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java index b2ef80902d1..7a30d54aa58 100644 --- a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java +++ b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java @@ -26,7 +26,6 @@ import com.yahoo.athenz.auth.Principal; import com.yahoo.athenz.auth.impl.SimplePrincipal; import com.yahoo.athenz.auth.util.Crypto; -import com.yahoo.athenz.common.server.http.HttpDriver; import com.yahoo.athenz.instance.provider.InstanceConfirmation; import com.yahoo.athenz.instance.provider.InstanceProvider; import com.yahoo.athenz.instance.provider.ResourceException; @@ -36,6 +35,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.security.PrivateKey; import java.security.interfaces.ECPrivateKey; import java.time.Instant; @@ -52,26 +52,22 @@ public class InstanceGithubActionsProviderTest { private final ClassLoader classLoader = this.getClass().getClassLoader(); - private static class InstanceGithubActionsProviderTestImpl extends InstanceGithubActionsProvider { - - HttpDriver httpDriver; - - public void setHttpDriver(HttpDriver httpDriver) { - this.httpDriver = httpDriver; - } - - @Override - HttpDriver getHttpDriver(String url) { - return httpDriver; - } - } - @AfterMethod public void tearDown() { System.clearProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_ENTERPRISE); System.clearProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_JWKS_URI); System.clearProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_AUDIENCE); System.clearProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_ENTERPRISE); + System.clearProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_ISSUER); + } + + static void createOpenIdConfigFile(File configFile, File jwksUri) throws IOException { + + final String fileContents = "{\n" + + " \"jwks_uri\": \"file://" + jwksUri.getCanonicalPath() + "\"\n" + + "}"; + Files.createDirectories(configFile.toPath().getParent()); + Files.write(configFile.toPath(), fileContents.getBytes()); } @Test @@ -84,53 +80,24 @@ public void testInitializeWithConfig() { provider.initialize("sys.auth.github_actions", "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); assertEquals(provider.getProviderScheme(), InstanceProvider.Scheme.CLASS); - assertNotNull(provider.getHttpDriver("https://config.athenz.io")); } @Test - public void testInitializeWithHttpDriver() throws IOException { - - // std test where the http driver will return null for the config object - - InstanceGithubActionsProviderTestImpl provider = new InstanceGithubActionsProviderTestImpl(); - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - provider.setHttpDriver(httpDriver); - provider.initialize("sys.auth.github_actions", - "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); - assertNotNull(provider); - - // test where the http driver will return a valid config object + public void testInitializeWithOpenIdConfig() throws IOException { - provider = new InstanceGithubActionsProviderTestImpl(); - httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet("/.well-known/openid-configuration", null)) - .thenReturn("{\"jwks_uri\":\"https://athenz.io/jwks\"}"); - provider.setHttpDriver(httpDriver); - provider.initialize("sys.auth.github_actions", - "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); - assertNotNull(provider); + File configFile = new File("./src/test/resources/config-openid/.well-known/openid-configuration"); + File jwksUriFile = new File("./src/test/resources/jwt-jwks.json"); + createOpenIdConfigFile(configFile, jwksUriFile); - // test when http driver return invalid data + System.setProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_ISSUER, "file://" + configFile.getCanonicalPath()); - provider = new InstanceGithubActionsProviderTestImpl(); - httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet("/.well-known/openid-configuration", null)) - .thenReturn("invalid-json"); - provider.setHttpDriver(httpDriver); - provider.initialize("sys.auth.github_actions", - "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); - assertNotNull(provider); - - // and finally throwing an exception + // std test where the http driver will return null for the config object - provider = new InstanceGithubActionsProviderTestImpl(); - httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet("/.well-known/openid-configuration", null)) - .thenThrow(new IOException("invalid-json")); - provider.setHttpDriver(httpDriver); + InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); provider.initialize("sys.auth.github_actions", "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); assertNotNull(provider); + Files.delete(configFile.toPath()); } @Test diff --git a/pom.xml b/pom.xml index a49594e9d88..c055337c688 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,7 @@ provider/aws/sia-eks provider/aws/sia-fargate provider/azure/sia-vm + provider/buildkite/sia-buildkite provider/gcp/sia-gce provider/gcp/sia-gke provider/gcp/sia-run diff --git a/provider/buildkite/sia-buildkite/Makefile b/provider/buildkite/sia-buildkite/Makefile new file mode 100644 index 00000000000..9e292893f41 --- /dev/null +++ b/provider/buildkite/sia-buildkite/Makefile @@ -0,0 +1,51 @@ +GOPKGNAME:=github.com/AthenZ/athenz/provider/buildkite/sia-buildkite +export GOPATH ?= /tmp/go +export GOPRIVATE=github.com + +FMT_LOG=/tmp/fmt.log + +BUILD_VERSION:=development + +all: build_darwin build_linux build_windows test + +build: get all + +mac: get build_darwin test + +linux: get build_linux test + +build_darwin: + @echo "Building darwin client with $(BUILD_VERSION)" + GOOS=darwin go install -ldflags "-X main.Version=$(BUILD_VERSION)" -v $(GOPKGNAME)/... + +build_linux: + @echo "Building linux client with $(BUILD_VERSION)" + GOOS=linux GOARCH=amd64 go install -ldflags "-X main.Version=$(BUILD_VERSION)" -v $(GOPKGNAME)/... + +build_windows: + @echo "Building windows client with $(BUILD_VERSION)" + GOOS=windows go install -ldflags "-X main.Version=$(BUILD_VERSION)" -v $(GOPKGNAME)/... + +get: + @echo "Getting dependencies..." + go get -t -d -tags testing $(GOPKGNAME)/... + +vet: + go vet $(GOPKGNAME)/... + +fmt: + gofmt -d . >$(FMT_LOG) + @if [ -s $(FMT_LOG) ]; then echo gofmt FAIL; cat $(FMT_LOG); false; fi + +test: vet fmt + go test -v $(GOPKGNAME)/... + +clean: + go clean -i -x $(GOPKGNAME)/... + +ubuntu: + sed -i.bak s/SIA_PACKAGE_VERSION/$(PACKAGE_VERSION)/g debian/sia/DEBIAN/control + mkdir -p debian/sia/usr/sbin/ + cp -fp $(GOPATH)/bin/siad debian/sia/usr/sbin/ + mkdir -p debian/pkg + cd debian && dpkg-deb --build sia pkg diff --git a/provider/buildkite/sia-buildkite/README.md b/provider/buildkite/sia-buildkite/README.md new file mode 100644 index 00000000000..21944f9448d --- /dev/null +++ b/provider/buildkite/sia-buildkite/README.md @@ -0,0 +1,20 @@ +# SIA for BuildKite + +The SIA utility must be installed in the BuildKite runtime to allow the BuildKite +to authenticate with Athenz and obtain the service identity x.509 certificate. + +``` +/usr/local/bin/sia -zts -domain -service -dns-domain -key-file -cert-file +``` + +The utility will generate a unique RSA private key and obtain a service identity x.509 certificate +from Athenz and store the key and certificate in the specified files. + +As part of its output, the agent shows the action and resource values that the domain administrator +must use to configure the Athenz services to allow the BuildKite runner to authorize: + +``` +2024/02/15 17:05:43 Action: build-kite.build +2024/02/15 17:05:43 Resource for main service: athens.builder:organization:my-org:pipeline:my-pipeline:ref:refs/heads/main:* +2024/02/15 17:05:43 Resource for PR service: athens.builder:organization:my-org:pipeline:my-pipeline:* +``` \ No newline at end of file diff --git a/provider/buildkite/sia-buildkite/authn.go b/provider/buildkite/sia-buildkite/authn.go new file mode 100644 index 00000000000..24f5d8ce6e4 --- /dev/null +++ b/provider/buildkite/sia-buildkite/authn.go @@ -0,0 +1,63 @@ +// +// 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 sia + +import ( + "crypto/rsa" + "fmt" + "github.com/AthenZ/athenz/libs/go/sia/util" + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" + "net/url" +) + +func GetOIDCTokenClaims(oidcToken string) (map[string]interface{}, error) { + signatureAlgorithms := []jose.SignatureAlgorithm{jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512, jose.ES256, jose.ES384, jose.ES512, jose.EdDSA} + tok, err := jwt.ParseSigned(oidcToken, signatureAlgorithms) + if err != nil { + return nil, fmt.Errorf("unable to parse BuildKite oidc token: %v", err) + } + + var claims map[string]interface{} + err = tok.UnsafeClaimsWithoutVerification(&claims) + if err != nil { + return nil, fmt.Errorf("unable to extract BuildKite oidc token claims: %v", err) + } + return claims, nil +} + +func GetCSRDetails(privateKey *rsa.PrivateKey, domain, service, provider, instanceId, dnsDomain, spiffeTrustDomain, subjC, subjO, subjOU string) (string, error) { + // note: RFC 6125 states that if the SAN (Subject Alternative Name) exists, + // it is used, not the CA. So, we will always put the Athenz name in the CN + // (it is *not* a DNS domain name), and put the host name into the SAN. + + var csrDetails util.CertReqDetails + csrDetails.CommonName = fmt.Sprintf("%s.%s", domain, service) + csrDetails.Country = subjC + csrDetails.OrgUnit = subjOU + csrDetails.Org = subjO + + csrDetails.HostList = []string{} + csrDetails.HostList = append(csrDetails.HostList, util.SanDNSHostname(domain, service, dnsDomain)) + + // add our uri fields. spiffe uri must be the first entry + csrDetails.URIs = []*url.URL{} + csrDetails.URIs = util.AppendUri(csrDetails.URIs, util.GetSvcSpiffeUri(spiffeTrustDomain, "default", domain, service)) + csrDetails.URIs = util.AppendUri(csrDetails.URIs, util.SanURIInstanceId(provider, instanceId)) + + return util.GenerateX509CSR(privateKey, csrDetails) +} diff --git a/provider/buildkite/sia-buildkite/authn_test.go b/provider/buildkite/sia-buildkite/authn_test.go new file mode 100644 index 00000000000..5154e4a6a5e --- /dev/null +++ b/provider/buildkite/sia-buildkite/authn_test.go @@ -0,0 +1,66 @@ +// +// 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 sia + +import ( + "crypto/rand" + "crypto/rsa" + "github.com/stretchr/testify/assert" + "os" + "strings" + "testing" +) + +func TestGetOIDCToken(t *testing.T) { + + validToken := "eyJraWQiOiJmNGI4MjE4MzdiNGVkY2JhNTYxMzZmMjJmMzdlZTY5Njk1MjBkZjIzNDA3MTI2Y2NlMTg4ZDQxNDFjMDE1ZDY4IiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FnZW50LmJ1aWxka2l0ZS5jb20iLCJzdWIiOiJvcmdhbml6YXRpb246dmVzcGFhaTpwaXBlbGluZTp2ZXNwYS1lbmdpbmUtdmVzcGEtam9ubXYtdGVzdDpyZWY6cmVmcy9oZWFkcy9tYXN0ZXI6Y29tbWl0OkhFQUQ6c3RlcDpwaXBlbGluZS10ZXN0IiwiYXVkIjoiaHR0cHM6Ly96dHMuYXRoZW56LmNkLnZlc3BhLWNsb3VkLmNvbTo0NDQzL3p0cy92MSIsImlhdCI6MTcyNDE1ODM4NCwibmJmIjoxNzI0MTU4Mzg0LCJleHAiOjE3MjQxNTg2ODQsIm9yZ2FuaXphdGlvbl9zbHVnIjoidmVzcGFhaSIsInBpcGVsaW5lX3NsdWciOiJ2ZXNwYS1lbmdpbmUtdmVzcGEtam9ubXYtdGVzdCIsImJ1aWxkX251bWJlciI6OSwiYnVpbGRfYnJhbmNoIjoibWFzdGVyIiwiYnVpbGRfY29tbWl0IjoiSEVBRCIsInN0ZXBfa2V5IjoicGlwZWxpbmUtdGVzdCIsImpvYl9pZCI6IjAxOTE2ZmQ4LWVlZWQtNDZhYS04MzA2LWI4YmY3YmI1MmE5MyIsImFnZW50X2lkIjoiMDE5MTZmZDktMDliMC00NDc4LTkzZWMtY2VlZmYyMDk3YTg1In0.c3TR_zGyG4Hjchk79q1CZBhNv56ahcxVOuHEpfUwD_HwYM7wdhm8XrtYnlKoMi8aBpirmGgBGqxtmswxz8YaVMDCf7OTI1S1SNRWNSuDYkjjWfKdXoAVTlV5yV6isgL-cb_hfDOv1Y-sWUvsG2_rtLTdqLDxtb5uxO6DqFPO8-fMqPsaWl3KygpAT7zo42szYncb0JOZe80ZADMWomp-ZnWTyZnNILfdxgyUb0KTbJWZva29F7vEMxhTPRgx2MMD3uqLv2xaMQlUF6hkX_mM8VyEZ_nBcKdw2kF9S77VSpAkICtbFdPaFTGI4QZJaBVQxbrVlrbAmwVt5p-DI_i-XQ" + + claims, err := GetOIDCTokenClaims(validToken) + assert.Nil(t, err) + + assert.Equal(t, "vespa-engine-vespa-jonmv-test", claims["pipeline_slug"].(string)) + assert.Equal(t, "vespaai", claims["organization_slug"].(string)) + assert.Equal(t, 9.0, claims["build_number"].(float64)) + assert.Equal(t, "01916fd8-eeed-46aa-8306-b8bf7bb52a93", claims["job_id"].(string)) + + subjectParts := strings.Split(claims["sub"].(string), ":") + assert.Equal(t, "organization", subjectParts[0]) + assert.Equal(t, "vespaai", subjectParts[1]) + assert.Equal(t, "pipeline", subjectParts[2]) + assert.Equal(t, "vespa-engine-vespa-jonmv-test", subjectParts[3]) + assert.Equal(t, "ref", subjectParts[4]) + assert.Equal(t, "refs/heads/master", subjectParts[5]) + + os.Clearenv() +} + +func TestGetOIDCTokenInvalidToken(t *testing.T) { + + _, err := GetOIDCTokenClaims("invalid-token") + assert.NotNil(t, err) + assert.Equal(t, "unable to parse BuildKite oidc token: go-jose/go-jose: compact JWS format must have three parts", err.Error()) + + os.Clearenv() +} + +func TestGetCSRDetails(t *testing.T) { + + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + csr, err := GetCSRDetails(privateKey, "sports", "api", "sys.auth.build-kite", "0001", "athenz.io", "athenz", "", "", "") + assert.Nil(t, err) + assert.True(t, csr != "") +} diff --git a/provider/buildkite/sia-buildkite/cmd/siad/main.go b/provider/buildkite/sia-buildkite/cmd/siad/main.go new file mode 100644 index 00000000000..e625e9ab9b1 --- /dev/null +++ b/provider/buildkite/sia-buildkite/cmd/siad/main.go @@ -0,0 +1,167 @@ +// +// 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 main + +import ( + "crypto/rand" + "crypto/rsa" + "flag" + "github.com/AthenZ/athenz/clients/go/zts" + "github.com/AthenZ/athenz/libs/go/sia/util" + "github.com/AthenZ/athenz/provider/buildkite/sia-buildkite" + "log" + "os" + "strconv" + "strings" +) + +// Following can be set by the build script using LDFLAGS + +var Version string + +func main() { + + var ztsURL, domain, service, spiffeTrustDomain, subjC, subjO, subjOU, provider string + var caCertFile, keyFile, certFile, signerCertFile, dnsDomain string + var oidcToken string + var showVersion bool + var expiryTime int + flag.StringVar(&oidcToken, "build-kite-token", "", "BuildKite OIDC token") + flag.StringVar(&keyFile, "key-file", "", "output private key file") + flag.StringVar(&certFile, "cert-file", "", "output certificate file") + flag.StringVar(&signerCertFile, "signer-cert-file", "", "output signer certificate file (optional)") + flag.StringVar(&domain, "domain", "", "domain of service") + flag.StringVar(&service, "service", "", "name of service") + flag.StringVar(&ztsURL, "zts", "", "url of the ZTS Service") + flag.StringVar(&dnsDomain, "dns-domain", "", "dns domain suffix to be included in the csr for sanDNS entries") + flag.StringVar(&subjC, "subj-c", "US", "Subject C/Country field (optional)") + flag.StringVar(&subjO, "subj-o", "", "Subject O/Organization field (optional)") + flag.StringVar(&subjOU, "subj-ou", "Athenz", "Subject OU/OrganizationalUnit field (optional)") + flag.StringVar(&provider, "provider", "sys.auth.build-kite", "Athenz Provider (optional)") + flag.StringVar(&caCertFile, "cacert", "", "CA certificate file (optional)") + flag.StringVar(&spiffeTrustDomain, "spiffe-trust-domain", "", "SPIFFE trust domain (optional)") + flag.IntVar(&expiryTime, "expiry-time", 360, "expiry time in minutes (optional)") + flag.BoolVar(&showVersion, "version", false, "Show version") + flag.Parse() + + if showVersion { + log.Printf("SIA BuildKite version: %s \n", Version) + os.Exit(0) + } + + // make sure all requires arguments are provided + if oidcToken == "" || keyFile == "" || certFile == "" || domain == "" || service == "" || ztsURL == "" || dnsDomain == "" { + log.Printf("missing required arguments\n") + flag.Usage() + os.Exit(1) + } + + // get the oidc token for the BuildKite agent + claims, err := sia.GetOIDCTokenClaims(oidcToken) + if err != nil { + log.Fatalf("unable to obtain oidc token claims from BuildKite: %v\n", err) + } + + // construct the instance id from the claims, as ::: + organizationSlug := claims["organization_slug"].(string) + if organizationSlug == "" { + log.Fatalf("unable to extract organization_slug from oidc token claims\n") + } + pipelineSlug := claims["pipeline_slug"].(string) + if pipelineSlug == "" { + log.Fatalf("unable to extract pipeline_slug from oidc token claims\n") + } + buildNumber := int(claims["build_number"].(float64)) + if buildNumber == 0 { + log.Fatalf("unable to extract build_number from oidc token claims\n") + } + jobId := claims["job_id"].(string) + if jobId == "" { + log.Fatalf("unable to extract job_id from oidc token claims\n") + } + instanceId := strings.Join([]string{organizationSlug, pipelineSlug, strconv.Itoa(buildNumber), jobId}, ":") + + subject := claims["sub"].(string) + if subject == "" { + log.Fatalf("unable to extract subject from oidc token claims\n") + } + // the resource for the main service is on the form: organization::pipeline::ref:refs/heads/:* + // while the resource for the PR service omits the ref part: organization::pipeline::* + subjectParts := strings.Split(subject, ":") + if len(subjectParts) < 6 { + log.Fatalf("invalid subject format: %s\n", subject) + } + mainResource := strings.Join(subjectParts[:6], ":") + ":*" + prResource := strings.Join(subjectParts[:4], ":") + ":*" + + // we're going to display the action and resource to be used in athenz policies + log.Printf("Action: %s\n", "build-kite.build") + log.Printf("Resource for main service: %s\n", domain+":"+mainResource) + log.Printf("Resource for PR service: %s\n", domain+":"+prResource) + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatalf("unable to generate rsa private key: %v\n", err) + } + + // generate a csr for this service + csrData, err := sia.GetCSRDetails(privateKey, domain, service, provider, instanceId, dnsDomain, spiffeTrustDomain, subjC, subjO, subjOU) + if err != nil { + log.Fatalf("unable to generate CSR: %v\n", err) + } + + // we're using copper argos which only uses tls and the attestation + // data contains the authentication details for the GitHub actions + client, err := util.ZtsClient(ztsURL, "", "", "", caCertFile) + if err != nil { + log.Fatalf("unable to create zts client: %v\n", err) + } + client.AddCredentials("User-Agent", "SIA-BuildKite "+Version) + + certExpiryTime := int32(expiryTime) + req := &zts.InstanceRegisterInformation{ + Provider: zts.ServiceName(provider), + Domain: zts.DomainName(domain), + Service: zts.SimpleName(service), + AttestationData: oidcToken, + Csr: csrData, + ExpiryTime: &certExpiryTime, + } + + // request a tls certificate for this service + identity, _, err := client.PostInstanceRegisterInformation(req) + if err != nil { + log.Fatalf("unable to register instance: %v\n", err) + } + + err = os.WriteFile(keyFile, util.GetPEMBlock(privateKey), 0400) + if err != nil { + log.Fatalf("unable to write private key file: %s - error: %v\n", keyFile, err) + } + + err = os.WriteFile(certFile, []byte(identity.X509Certificate), 0444) + if err != nil { + log.Fatalf("unable to write certificate file: %s - error: %v\n", certFile, err) + } + + if signerCertFile != "" { + err = os.WriteFile(signerCertFile, []byte(identity.X509CertificateSigner), 0444) + if err != nil { + log.Fatalf("unable to write signer certificate file: %s - error: %v\n", signerCertFile, err) + } + } +} diff --git a/provider/buildkite/sia-buildkite/debian/sia/DEBIAN/control b/provider/buildkite/sia-buildkite/debian/sia/DEBIAN/control new file mode 100644 index 00000000000..f3a6c0f769d --- /dev/null +++ b/provider/buildkite/sia-buildkite/debian/sia/DEBIAN/control @@ -0,0 +1,5 @@ +Package: sia-build-kite +Version: SIA_PACKAGE_VERSION +Maintainer: cncf-athenz-maintainers@lists.cncf.io +Architecture: amd64 +Description: BuildKite Athenz Service Identity Agent diff --git a/provider/buildkite/sia-buildkite/pom.xml b/provider/buildkite/sia-buildkite/pom.xml new file mode 100644 index 00000000000..cc56b377366 --- /dev/null +++ b/provider/buildkite/sia-buildkite/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + com.yahoo.athenz + athenz + 1.11.65-SNAPSHOT + ../../../pom.xml + + + sia-build-kite + jar + sia-build-kite + Service Identity Agent for BuildKite + + + true + true + + + + + + org.codehaus.mojo + exec-maven-plugin + ${maven-exec-plugin.version} + + + + exec + + compile + + + + make + + clean + all + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + default-jar + + + + + + +