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
1 change: 1 addition & 0 deletions sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Breaking Changes

### Bugs Fixed
- Fixed an issue where a disabled certificate in Azure Key Vault caused keystore initialization to fail with an HTTP 403 error. Disabled certificates are now skipped when loading aliases and a warning is logged for each skipped certificate. [#49730](https://github.com/Azure/azure-sdk-for-java/pull/49730)

Comment thread
moarychan marked this conversation as resolved.
### Other Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ public List<String> getAliases() {
for (CertificateItem certificateItem : certificateListResult.getValue()) {
String id = certificateItem.getId();
String alias = getCertificateNameFromCertificateItemId(id);

// Skip certificates that are explicitly disabled in Key Vault. Attempting to load a disabled
// certificate's key/secret later would fail with an HTTP 403 and break keystore initialization.
if (!certificateItem.isEnabled()) {
LOGGER.log(WARNING, "Skipping disabled certificate with alias: {0}", alias);
continue;
}

result.add(alias);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class CertificateItem implements JsonSerializable<CertificateItem> {
*/
private String id;

/**
* Stores the management attributes of the certificate.
*/
private CertificateItemAttributes attributes;

/**
* Get the id.
*
Expand All @@ -37,10 +42,42 @@ public void setId(String id) {
this.id = id;
}

/**
* Get the management attributes of the certificate.
*
* @return the management attributes of the certificate, or {@code null} if not specified.
*/
public CertificateItemAttributes getAttributes() {
return attributes;
}

/**
* Set the management attributes of the certificate.
*
* @param attributes the management attributes of the certificate.
*/
public void setAttributes(CertificateItemAttributes attributes) {
this.attributes = attributes;
}

/**
* Indicates whether the certificate is enabled.
* <p>
* A certificate is considered enabled unless its attributes explicitly mark it as disabled (i.e.
* {@code attributes.enabled == false}). When the attributes or the {@code enabled} flag are absent, the certificate
* is treated as enabled for backward compatibility.
*
* @return {@code false} only when the certificate is explicitly disabled, otherwise {@code true}.
*/
public boolean isEnabled() {
return attributes == null || !Boolean.FALSE.equals(attributes.isEnabled());
}

@Override
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
jsonWriter.writeStartObject();
jsonWriter.writeStringField("id", this.id);
jsonWriter.writeJsonField("attributes", this.attributes);

return jsonWriter.writeEndObject();
}
Expand All @@ -66,6 +103,8 @@ public static CertificateItem fromJson(JsonReader jsonReader) throws IOException

if ("id".equals(fieldName)) {
deserializedCertificateItem.id = reader.getString();
} else if ("attributes".equals(fieldName)) {
deserializedCertificateItem.attributes = CertificateItemAttributes.fromJson(reader);
} else {
reader.skipChildren();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.security.keyvault.jca.implementation.model;

import com.azure.json.JsonReader;
import com.azure.json.JsonSerializable;
import com.azure.json.JsonToken;
import com.azure.json.JsonWriter;

import java.io.IOException;

/**
* The management attributes of a {@link CertificateItem}, as returned by the Azure Key Vault "list certificates" REST
* API.
*/
public class CertificateItemAttributes implements JsonSerializable<CertificateItemAttributes> {
/**
* Stores whether the certificate is enabled.
*/
private Boolean enabled;

/**
* Get whether the certificate is enabled.
*
* @return whether the certificate is enabled, or {@code null} if the attribute was not specified.
*/
public Boolean isEnabled() {
return enabled;
}

/**
* Set whether the certificate is enabled.
*
* @param enabled whether the certificate is enabled.
*/
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}

@Override
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
jsonWriter.writeStartObject();
jsonWriter.writeBooleanField("enabled", this.enabled);

return jsonWriter.writeEndObject();
}

/**
* Reads an instance of {@link CertificateItemAttributes} from the {@link JsonReader}.
*
* @param jsonReader The {@link JsonReader} being read.
*
* @return An instance of {@link CertificateItemAttributes} if the {@link JsonReader} was pointing to an instance of
* it, or {@code null} if it was pointing to JSON {@code null}.
*
* @throws IOException If an error occurs while reading the {@link CertificateItemAttributes}.
*/
public static CertificateItemAttributes fromJson(JsonReader jsonReader) throws IOException {
return jsonReader.readObject(reader -> {
CertificateItemAttributes deserializedCertificateItemAttributes = new CertificateItemAttributes();

while (reader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = reader.getFieldName();

reader.nextToken();

if ("enabled".equals(fieldName)) {
deserializedCertificateItemAttributes.enabled = reader.getNullable(JsonReader::getBoolean);
} else {
reader.skipChildren();
}
}

return deserializedCertificateItemAttributes;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.azure.security.keyvault.jca.PropertyConvertorUtils;
import com.azure.security.keyvault.jca.implementation.model.AccessToken;
import com.azure.security.keyvault.jca.implementation.model.CertificateItem;
import com.azure.security.keyvault.jca.implementation.model.CertificateItemAttributes;
import com.azure.security.keyvault.jca.implementation.model.CertificateListResult;
import com.azure.security.keyvault.jca.implementation.utils.AccessTokenUtil;
import com.azure.security.keyvault.jca.implementation.utils.HttpUtil;
Expand All @@ -19,6 +20,7 @@
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap;
Expand Down Expand Up @@ -109,6 +111,97 @@ public void testGetAliasWithCertificateInfoWith2Pages() {
}
}

@Test
public void testGetAliasFiltersOutDisabledCertificate() {
try (MockedStatic<HttpUtil> utilities = Mockito.mockStatic(HttpUtil.class)) {
utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod();
utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod();

// Enabled certificate.
CertificateItemAttributes enabledAttributes = new CertificateItemAttributes();
enabledAttributes.setEnabled(true);
CertificateItem enabledCertificate = new CertificateItem();
enabledCertificate.setId("certificates/client-cert-active");
enabledCertificate.setAttributes(enabledAttributes);

// Disabled certificate. This one previously caused an HTTP 403 while initializing the keystore.
CertificateItemAttributes disabledAttributes = new CertificateItemAttributes();
disabledAttributes.setEnabled(false);
CertificateItem disabledCertificate = new CertificateItem();
disabledCertificate.setId("certificates/client-cert-unused");
disabledCertificate.setAttributes(disabledAttributes);

CertificateListResult certificateListResult = new CertificateListResult();
certificateListResult.setValue(Arrays.asList(enabledCertificate, disabledCertificate));

String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(certificateListResultString);

KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null);
List<String> result = keyVaultClient.getAliases();

assertEquals(1, result.size());
assertTrue(result.contains("client-cert-active"));
assertFalse(result.contains("client-cert-unused"));
}
}

@Test
public void testGetAliasKeepsEnabledAndAttributelessCertificates() {
try (MockedStatic<HttpUtil> utilities = Mockito.mockStatic(HttpUtil.class)) {
utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod();
utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod();

// Certificate explicitly enabled.
CertificateItemAttributes enabledAttributes = new CertificateItemAttributes();
enabledAttributes.setEnabled(true);
CertificateItem enabledCertificate = new CertificateItem();
enabledCertificate.setId("certificates/enabledCertificate");
enabledCertificate.setAttributes(enabledAttributes);

// Certificate without attributes, which must be treated as enabled for backward compatibility.
CertificateItem attributelessCertificate = new CertificateItem();
attributelessCertificate.setId("certificates/attributelessCertificate");

CertificateListResult certificateListResult = new CertificateListResult();
certificateListResult.setValue(Arrays.asList(enabledCertificate, attributelessCertificate));

String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(certificateListResultString);

KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null);
List<String> result = keyVaultClient.getAliases();

assertEquals(2, result.size());
assertTrue(result.containsAll(Arrays.asList("enabledCertificate", "attributelessCertificate")));
}
}

@Test
public void testGetAliasFiltersDisabledCertificateFromRawResponse() {
try (MockedStatic<HttpUtil> utilities = Mockito.mockStatic(HttpUtil.class)) {
utilities.when(() -> HttpUtil.validateUri(anyString(), anyString())).thenCallRealMethod();
utilities.when(() -> HttpUtil.addTrailingSlashIfRequired(anyString())).thenCallRealMethod();

// A response that mirrors the shape returned by the Azure Key Vault "list certificates" REST API, with one
// enabled and one disabled certificate.
String rawResponse = "{\"value\":["
+ "{\"id\":\"https://fake.vault.azure.net/certificates/client-cert-active\","
+ "\"attributes\":{\"enabled\":true,\"nbf\":1783324860,\"exp\":1814861460}},"
+ "{\"id\":\"https://fake.vault.azure.net/certificates/client-cert-unused\","
+ "\"attributes\":{\"enabled\":false,\"nbf\":1783324860,\"exp\":1814861460}}]," + "\"nextLink\":null}";

utilities.when(() -> HttpUtil.get(notNull(), anyMap())).thenReturn(rawResponse);

KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null);
List<String> result = keyVaultClient.getAliases();

assertEquals(1, result.size());
assertTrue(result.contains("client-cert-active"));
assertFalse(result.contains("client-cert-unused"));
}
}

@Test
public void testCacheToken() {
try (MockedStatic<AccessTokenUtil> tokenUtilMockedStatic = Mockito.mockStatic(AccessTokenUtil.class);
Expand Down
Loading