After some research, and discussions with Microsoft, the way to use this method and retain the certificates is to query the service principal's key credentials using a regular GET call to https://graph.microsoft.com/v1.0/servicePrincipals/{id}
. Then, create a new JSON payload with both the CURRENT key credentials from the GET call and the NEW key credentials. You can then make a PATCH call, using the same URI, with that payload and it will successfully update the service principal certificates without deleting the current certificates.
Couple of things to note:
The "customKeyIdentifier" field has a character limit and since it is recommended to use a hash of the thumbprint I tried SHA256 but it created too long of a hash, so I used SHA1. Since this isn't as secure another random generation could work as long as it is within the limit, though a GUID didn't work and I suspect it was a character type issue with the dashes. I have not done enough testing to find the limit, though my identifier was 40 characters and it was successful.
For the private key you can use the following command in Powershell to get the correct decoded private key. Here is where I found this command. (This is the only method I found useful as OpenSSL didn't seem to output the correct private key Azure is looking for)
$fileContentBytes = get-content '<PATH_TO_PFX_FILE>' -asbytestream
$privKey = [system.convert]::tobase64string($fileContentBytes)
Here is an example of a successful JSON payload with existing certs and a new cert. The password credentials section should contain the passphrase for the new cert as well as the keyID of the private key. This specific service principal has two current certificates.
Please note that while the current certs only have a "AsymmetricX509Cert" for the type, the new cert has both "AsymmetricX509Cert" and "X509CertandPassword" using the public key and private key respectively.
{
"keyCredentials": [
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"displayName": "<CN>",
"endDateTime": "<EXPIRY_DATE>",
"key": null <EXISTING_CERT>,
"keyId": "<GUID>",
"startDateTime": "<START_DATE>",
"type": "AsymmetricX509Cert",
"usage": "Verify"
},
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"displayName": "<CN>",
"endDateTime": "<EXPIRY_DATE>",
"key": null <EXISTING_CERT>,
"keyId": "<GUID>",
"startDateTime": "<START_DATE>",
"type": "AsymmetricX509Cert",
"usage": "Sign"
},
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"displayName": "<CN>",
"endDateTime": "<EXPIRY_DATE>",
"key": null <EXISTING_CERT>,
"keyId": "<GUID>",
"startDateTime": "<START_DATE>",
"type": "AsymmetricX509Cert",
"usage": "Verify"
},
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"displayName": "<CN>",
"endDateTime": "<EXPIRY_DATE>",
"key": null <EXISTING_CERT>,
"keyId": "<GUID>",
"startDateTime": "<START_DATE>",
"type": "AsymmetricX509Cert",
"usage": "Sign"
},
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"endDateTime": "<EXPIRY_DATE>",
"type": "X509CertandPassword",
"key": "<NEWCERT-PrivateKey>",
"displayName": "<CN>",
"startDateTime": "<START_DATE>",
"keyId": "<GUID>",
"usage": "Sign"
},
{
"customKeyIdentifier": "<HASH_OF_THUMBPRINT>",
"endDateTime": "<EXPIRY_DATE>",
"type": "AsymmetricX509Cert",
"key": "<NEWCERT-PublicKey>",
"displayName": "<CN>",
"startDateTime": "<START_DATE>",
"keyId": "<GUID>",
"usage": "Verify"
}
],
"passwordCredentials": [
{
"endDateTime": "<EXPIRY_DATE>",
"secretText": "<PASSPHRASE_FOR_NEW_CERT>",
"startDateTime": "<START_DATE>",
"keyId": "<KEY_ID_OF_PRIVATE_KEY>",
"customKeyIdentifier": "<HASH_OF_THUMBPRINT_OF_NEW_CERT>"
}
]
}