Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
test-entra-node test-entra-node-local \
test-apim-java \
test-cosmos test-cosmos-mongo test-cosmos-postgresql test-cosmos-cassandra test-cosmos-gremlin test-cosmos-table test-cosmos-nosql test-cosmos-all \
test-sql test-mysql test-mariadb test-terraform-compat test-opentofu-compat test-azcli test-iac-compat compat-docker test-compat clean
test-sql test-mysql test-mariadb test-terraform-compat test-opentofu-compat test-azcli test-iac-compat compat-docker test-compat clean \
smoke-native-crypto

MVN = ./mvnw
PORT = 4577
Expand Down Expand Up @@ -470,6 +471,30 @@ test: build
$(MVN) test
$(MAKE) compat-docker

# ── Native-Image Crypto Smoke Gate ────────────────────────────────────────────

smoke-native-crypto:
$(MVN) package -Dnative -DskipTests -B -Dquarkus.native.additional-build-args-append="-Ob" -q
./target/*-runner & echo $$! > /tmp/floci-az-native.pid
@echo "Waiting for floci-az native runner on port $(PORT)..."
@EXIT=0; \
ATTEMPTS=0; \
until curl -sf http://localhost:$(PORT)/health > /dev/null 2>&1; do \
ATTEMPTS=$$((ATTEMPTS + 1)); \
if [ $$ATTEMPTS -ge 120 ]; then \
echo "floci-az native runner did not become healthy after 120s" >&2; \
EXIT=1; \
break; \
fi; \
sleep 1; \
done; \
if [ $$EXIT -eq 0 ]; then \
bash scripts/native-crypto-smoke.sh || EXIT=$$?; \
fi; \
kill $$(cat /tmp/floci-az-native.pid 2>/dev/null) 2>/dev/null || true; \
rm -f /tmp/floci-az-native.pid; \
exit $$EXIT

# ── Cleanup ───────────────────────────────────────────────────────────────────

clean:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ flowchart LR
| **App Configuration** | `/{account}-appconfig/` | Key-values, labels, feature flags, snapshots (async provisioning), revisions, locks, ETags; pagination (`@nextLink`), `$select`, `tags` filtering, `Accept-Datetime` time-travel, `Sync-Token` |
| **Cosmos DB (NoSQL)** | `/{account}-cosmos/` | Databases, containers, documents CRUD + full SQL queries: always-on, no Docker. PATCH; transactional batch. |
| **Cosmos DB NoSQL (embedded)** | `/{account}-cosmos-nosql/` | Same embedded SQL engine as above, exposed as a named engine endpoint. Opt-in with `FLOCI_AZ_SERVICES_COSMOS_ENGINES_NOSQL_ENABLED=true`; no Docker required. |
| **Key Vault** | `/{account}-keyvault/` | Secrets CRUD, versioning, soft-delete, properties update |
| **Key Vault** | `/{account}-keyvault/` | Secrets CRUD, versioning, soft-delete, properties update; Keys CRUD, backup/restore, rotation, RSA/EC/oct crypto (encrypt/decrypt/sign/verify/wrap/unwrap), `/rng`; Managed HSM (`/{account}-managedhsm/`) |
| **Event Hubs** | AMQP `:5672` / Kafka `:9093` | AMQP 1.0 (Artemis sidecar), Kafka-compatible (Redpanda, opt-in) |
| **Service Bus** | `/{account}-servicebus/` + AMQP `:5673` | Queues, topics, subscriptions (created dynamically); AMQP 1.0 data plane via Artemis sidecar, or mocked (management plane only) |
| **Azure SQL Database** | ARM path + `/{account}-sql/` | Servers, databases, firewall rules; ARM-only by default, optional managed SQL Server 2025 containers |
Expand Down
89 changes: 89 additions & 0 deletions compatibility-tests/compat-azcli/test/keyvault.bats
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,92 @@ setup() {
assert_success
assert_equal "$(echo "$output" | jq -r '.value')" "hello-from-az-cli"
}

@test "az keyvault: key create/show/list/delete round-trip (data-plane)" {
run az keyvault key create --vault-name "$KV_NAME" -n "$KEY_NAME" \
--kty RSA --size 2048 -o none
if [ "$status" -ne 0 ]; then
skip "key vault data-plane not reachable: $output"
fi

run az_json keyvault key show --vault-name "$KV_NAME" -n "$KEY_NAME"
assert_success
assert_equal "$(echo "$output" | jq -r '.key.kty')" "RSA"

run az_json keyvault key list --vault-name "$KV_NAME"
assert_success
[ -n "$(echo "$output" | jq -r '.[].kid')" ]

run az keyvault key delete --vault-name "$KV_NAME" -n "$KEY_NAME" -o none
assert_success
}

@test "az keyvault: key set-attributes + encrypt/decrypt + rotation-policy (data-plane)" {
run az keyvault key create --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--kty RSA --size 2048 -o none
if [ "$status" -ne 0 ]; then
skip "key vault data-plane not reachable: $output"
fi

# set-attributes: disable the key and verify the flag is reflected on show.
run az keyvault key set-attributes --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--enabled false -o none
assert_success
run az_json keyvault key show --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME"
assert_success
assert_equal "$(echo "$output" | jq -r '.attributes.enabled')" "false"

# re-enable so encrypt/decrypt can proceed.
run az keyvault key set-attributes --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--enabled true -o none
assert_success

# encrypt/decrypt round-trip.
local plaintext="hello-az-cli"
local b64
b64=$(printf '%s' "$plaintext" | base64)
run az_json keyvault key encrypt --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--algorithm RSA-OAEP-256 --value "$b64"
assert_success
local ciphertext
ciphertext=$(echo "$output" | jq -r '.result')
[ -n "$ciphertext" ]

run az_json keyvault key decrypt --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--algorithm RSA-OAEP-256 --value "$ciphertext"
assert_success
assert_equal "$(echo "$output" | jq -r '.result' | base64 -d)" "$plaintext"

# rotation-policy show + update round-trip.
run az_json keyvault key rotation-policy show --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME"
assert_success
run az_json keyvault key rotation-policy update --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" \
--value '{"lifetimeActions":[{"trigger":{"timeAfterCreate":"P30D"},"action":{"type":"Rotate"}}],"attributes":{"expiryTime":"P90D"}}'
assert_success

run az keyvault key delete --vault-name "$KV_NAME" -n "$CRYPTO_KEY_NAME" -o none
assert_success
}

@test "az keyvault: key soft-delete lifecycle (data-plane)" {
run az keyvault key create --vault-name "$KV_NAME" -n "$LIFECYCLE_KEY_NAME" \
--kty RSA --size 2048 -o none
if [ "$status" -ne 0 ]; then
skip "key vault data-plane not reachable: $output"
fi

run az keyvault key delete --vault-name "$KV_NAME" -n "$LIFECYCLE_KEY_NAME" -o none
assert_success

run az_json keyvault key list-deleted --vault-name "$KV_NAME"
assert_success
[ -n "$(echo "$output" | jq -r '.[].kid')" ]

run az keyvault key recover --vault-name "$KV_NAME" -n "$LIFECYCLE_KEY_NAME" -o none
assert_success

run az keyvault key delete --vault-name "$KV_NAME" -n "$LIFECYCLE_KEY_NAME" -o none
assert_success
run az keyvault key purge --vault-name "$KV_NAME" -n "$LIFECYCLE_KEY_NAME" -o none
assert_success
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export CONTAINER_NAME="floci-test-container"
export BLOB_NAME="hello.txt"
export KV_NAME="floci-test-kv"
export SECRET_NAME="floci-test-secret"
export KEY_NAME="floci-test-key"
export CRYPTO_KEY_NAME="floci-test-crypto-key"
export LIFECYCLE_KEY_NAME="floci-test-lifecycle-key"
export VNET_NAME="floci-test-vnet"
export SUBNET_NAME="floci-test-subnet"
export NIC_NAME="floci-test-nic"
Expand Down
4 changes: 4 additions & 0 deletions compatibility-tests/sdk-test-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-secrets</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-keys</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
import com.azure.messaging.servicebus.ServiceBusClientBuilder;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.security.keyvault.keys.KeyClient;
import com.azure.security.keyvault.keys.KeyClientBuilder;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.qpid.jms.JmsConnectionFactory;
Expand Down Expand Up @@ -184,6 +188,35 @@ public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineN
}
}

/**
* The Azure SDKs require a {@code https://{account}.vault.azure.net/keys/...} key identifier for
* cryptography clients and derive the request URL from its host, dropping any path component. The
* emulator is reached over path-based routing instead, so this policy rewrites each crypto request
* from the host-based URL to {@code http://{endpoint}/{account}-keyvault/keys/...}.
*/
static final class KeyVaultDataPlanePolicy implements HttpPipelinePolicy {
private final String accountPath;

KeyVaultDataPlanePolicy(String accountPath) {
this.accountPath = accountPath;
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
URL url = context.getHttpRequest().getUrl();
URI endpoint = URI.create(BASE);
try {
String path = "/" + accountPath + url.getPath();
String query = url.getQuery();
context.getHttpRequest().setUrl(new URL("http", endpoint.getHost(), endpoint.getPort(),
path + (query != null ? "?" + query : "")));
} catch (MalformedURLException e) {
return Mono.error(e);
}
return next.process();
}
}

// ── Event Hubs / AMQP ────────────────────────────────────────────────────

static final String EVENTHUB_HOST =
Expand Down Expand Up @@ -318,6 +351,53 @@ static SecretClient buildKeyVaultClient() {
.buildClient();
}

static KeyClient buildKeyClient() {
String vaultUrl = keyVaultUrl();
return new KeyClientBuilder()
.vaultUrl(vaultUrl)
.credential(req -> Mono.just(new AccessToken("fake-token", OffsetDateTime.now().plusHours(1))))
.addPolicy(new ForceHttpPolicy())
.disableChallengeResourceVerification()
.buildClient();
}

/** Managed HSM data-plane client: same handler, but routed via the {@code -managedhsm} suffix. */
static KeyClient buildManagedHsmKeyClient() {
String vaultUrl = BASE.replace("http://", "https://") + "/" + ACCOUNT + "-managedhsm";
return new KeyClientBuilder()
.vaultUrl(vaultUrl)
.credential(req -> Mono.just(new AccessToken("fake-token", OffsetDateTime.now().plusHours(1))))
.addPolicy(new ForceHttpPolicy())
.disableChallengeResourceVerification()
.buildClient();
}

static CryptographyClient buildCryptographyClient(String keyName, String keyVersion) {
return buildCryptographyClient(keyName, keyVersion, false);
}

/** Managed HSM cryptography client: same handler, routed via the {@code -managedhsm} suffix. */
static CryptographyClient buildManagedHsmCryptographyClient(String keyName, String keyVersion) {
return buildCryptographyClient(keyName, keyVersion, true);
}

private static CryptographyClient buildCryptographyClient(String keyName, String keyVersion, boolean hsm) {
String host = hsm
? "https://" + ACCOUNT + ".managedhsm.azure.net"
: "https://" + ACCOUNT + ".vault.azure.net";
String keyIdentifier = host + "/keys/" + keyName + "/" + keyVersion;
return new CryptographyClientBuilder()
.keyIdentifier(keyIdentifier)
.credential(req -> Mono.just(new AccessToken("fake-token", OffsetDateTime.now().plusHours(1))))
.addPolicy(new KeyVaultDataPlanePolicy(ACCOUNT + (hsm ? "-managedhsm" : "-keyvault")))
.disableChallengeResourceVerification()
.buildClient();
}

private static String keyVaultUrl() {
return BASE.replace("http://", "https://") + "/" + ACCOUNT + "-keyvault";
}

// ── Service Bus ───────────────────────────────────────────────────────────

static final String SERVICEBUS_HOST =
Expand Down
Loading