When I want to follow the tutorial : https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-saml-sso-configure-api#add-a-custom-signing-key.
I am stuck on updating the service to add my private key and certificate. I don't want to have to click on the azure interface, I want to do everything via Graph API (it's for automatic tests). I don't use Windows and therefore Powershell for my script, I use Python and OpenSSL on Linux.
'Request_BadRequest', 'Invalid certificate: Key value is invalid certificate', status: 400
The data sent:
"keyCredentials": [
{
"customKeyIdentifier": "Y6p0Dm1eBwzsa7P1xIObqsLUj6A=",
"keyId": "e4ba4cbd-8bfc-4c3a-a6a7-b693c52dc807",
"type": "AsymmetricX509Cert",
"usage": "Sign",
"key": "MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIlPjaWgWswX4CAggAMAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECEPFi6NFq0hMBIIEyGzmtT3UF9WDM5bGIMrOgXqf6XBtTwEZhnLrbMGk2GupjpS49M26uS2QEJq6ZGRitf8+7UH4g3dhBxk+35/18+20Z4Nvu661Xzo/Kod9rWPhoHBLg5hjL5s/gfuMu3p2bDswhijCqDAmNaXOCBifhba3ECM3NRmr4mOC/u+WNa+CuK0ihU9VlaU0Jx/LsgIN1PLZnrPtUTxvNXG7oGftEjDy/wu5RnLuLuQr5R5FSoa1TCHI8Q81loSkPSpRMQtC06jp63IjqIc1K81qrW7VE9nRGgeM0xlSHIQTJ1mpWfQm97RJbCvdORy8+MgceZXd5/Yr4Lb+bL2zT3lpr83XQgFwC8l8uGK1lcScaXWn6Smquy9q3uBHgedS3g4sB4aF4l5MD4WwOFXOcLhkZIV/t7M5aRlHyMoWMo5ZHbPqEMQdc3Dj82WvYHW6WV7XxEFs+a25lgVWUh42dnanEGu0ViKa39oKEVXUHa8Q42Jv3q9eO4NmnD9CKiuZA1/k9edwGHIxeXUcqW1oFcxjOw23XJmQ9F6G38Ei4u2ECFpvqlZzHBcGEjKcUyjO8LCP+NZxJfjgMgOR4hMfpQuzaamV9CErSVEckCQG6yAMeJPmQmwtkNIXnMR/uw3hm72nGnZUCxRhtvcdbfrP0DWG7Vs6tyjEGjBm4r3pc7bXWMZ1KBrB6OMLSvYh2Ltyp/CZ+I2bzUmp2+bFQmvEVXfPdetz8X9YEn9/GbjgGiof4ZMrCOeDX06DRu9CKq7HHYP6mnhjWPGXEXR7h887v49LjM0A4NpPIcRNjta1l89spA3gVa9ffylY5sqrQM8+4YR0IsxvaztYUcZQoFXaMwffFn1I1ef9NJHYTsbX8ZDBTGOW+gcg7hIU/a+7Fa4lBtkxi1zRHnzzMeLyHOS5Lv4gmVg6Z3TJnHsaGS/oT4/QZJIsAtvaleCNPKMeFByMFCRHMk6rzqe+x47XH34UP6qVg+af8eioCkM/H20VddidIKKkkTmVZbc/FfkdnzXpksKh+foFvQVFSmXLj7zQMPW+lNt/B4eq81mJfV2uWRBI6+NUjmRTX028hPBOvpx3EMgyENXjrN01yCBVWcVH1lLW+W6laLdVqIilsgPur+86aIvlIqGCxSHw4AfeRyyiF1Qh9C+7v8NjC9hGSJfIgQjQ790CK2Iro7ukNHC0OWbkHtkurZvyclWd8r7DaKqeJFaSik6MibuJM2mW7Vr4SCOUSkfLhFFUGZG8QU9L4h3FPlKp8o5eo2sLY7ybNIgs5FSyShv2v2OWD2GkatGwcqPD1yJO0WZ+Pgp9iaVH+AfA08B3S0R0CfQAJE+onYiH/glpEyxLKanCwnmkCqrebkdHBfCWNsoNlIUr7D11puu4DWaJR0wmLUgjwCKy6by5ZqyGR5hXzk4WdnouhwJrFwJciSdDyT3osi0XDl0oYXb20aFvMRdBpn6W+7e2DOe+xA4S8LPM+3vSmO3u/i0beojzj27g3tvnJdEyxMpyCKPJskwqXurt9J1POf1JLQ/nOQOqfM00fZinjIQlIl2+nOppzcWav5yzS3TLi+AGdNYs0bxbRbC5pzXNGssLwi4d+Hdey4nxtduZDu6rRQbmzlurp40u6/MO5bolja4krA=="
},
{
"customKeyIdentifier": "Y6p0Dm1eBwzsa7P1xIObqsLUj6A=",
"keyId": "5680e704-01d8-4a1a-bf6f-79e6794bc894",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"key": "MIIDrzCCApegAwIBAgIUdVKPcuuIZEmXiyWvKNRE3j9aj/UwDQYJKoZIhvcNAQELBQAwZzELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lERjEOMAwGA1UEBwwFUGFyaXMxDjAMBgNVBAoMBU9sZmVvMRcwFQYDVQQLDA5SJkQgRGVwYXJ0bWVudDERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjAwNzIwMDY1MjE0WhcNMjAwODA0MDY1MjE0WjBnMQswCQYDVQQGEwJGUjEMMAoGA1UECAwDSURGMQ4wDAYDVQQHDAVQYXJpczEOMAwGA1UECgwFT2xmZW8xFzAVBgNVBAsMDlImRCBEZXBhcnRtZW50MREwDwYDVQQDDAh0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkCMYK54Z/LiccB6U5szncQJFWhfc4d3jRqeySpngGJ56UR8Jxn5uwwyfx09eanauwNY4yCR82YvMAnfh7z6+cY2B6MiqfAQ9ghETCEy+TzXJIpHaMO95rTZGJJhiLcJsjmWCQjko26Bfr0kuMykks2vIHcANxpkufNqQ7ZHHAtaXZHtnjQY0DHJGz39BVjElqzkUBs+rmRDIojpDM1aclU2BTWLyYysVg43S6Loa20jETj9Z5INwDPK9ah7dVJZQ2zhyJ+KiShdGw+FjTF3sAXcoGRHZWqZalGrQx6UmegJxRGEZwAYat60jM9FTPs69ETjkkeQoxZG6gEZNbsFosCAwEAAaNTMFEwHQYDVR0OBBYEFDC9PDYIGVTgNEWTwRsBtRLCYL43MB8GA1UdIwQYMBaAFDC9PDYIGVTgNEWTwRsBtRLCYL43MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH6yOYj1J22g7+TtgUuY/OaKzOCz4aJ4BE46FCSd5YaMQjk1RpnnKCnmz/QVo7sM2AaHPJpg3H4L77ZeeknsqWvTTmIwXDvyVpxBEp96qyamzTcxYOzrx8hRHrHzxkCvqj6I6EEAwnsYkJv62qhPDQ/uVE/oNXloqQQ7IE7Xvb7ZUtO+fkzClby5dimk/UbMgvkvOnHFcDrtc+xndwINc5PB0DF0oMKozImiTXfuPij/7Q6WKCFUeURKgnSX6WM/OuLQhBCljnaABpSYl6VFZMQbpH2im8s2iQTNEfwvcbHx/jCeVxaslBje6/tqIhrPK/GIt8io9Rr69a6qzDA6nuM="
}
],
"passwordCredentials": [
{
"customKeyIdentifier": "Y6p0Dm1eBwzsa7P1xIObqsLUj6A=",
"keyId": "e4ba4cbd-8bfc-4c3a-a6a7-b693c52dc807",
"endDateTime": "2020-07-24T08:52:14.869829",
"startDateTime": "2020-07-16T08:52:14.869854",
"secretText": "vypgoyylxxortmcc"
}
],
"preferredTokenSigningKeyThumbprint": "63AA740E6D5E070CEC6BB3F5C4839BAAC2D48FA0",
"preferredSingleSignOnMode": "saml",
"notificationEmailAddresses": [
"fake@test.com"
]
}
Tests done:
- When I put just the public key it works.
- When I want to put a private key without password it doesn't works, I have this message : The value for the property 'usage' in one of your credentials is invalid. Acceptable values are Sign, Verify.
- when I manually upload the pfx generated from the certificate and the private key in azure it works.
Code for generated data (WIP)
import json
import os
import subprocess
import uuid
from datetime import datetime, timedelta
from typing import Optional
def random_string(string_length: int = 8) -> str:
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(string_length))
def gen_cert_and_private_key(
dirname: str, encrypted: bool, password: Optional[str] = None
):
if encrypted:
if not password:
password = random_string(16)
password_option = f"-passout pass:{password}"
else:
password_option = "-nodes"
certificate = dirname + "/certificate.crt"
private_key = dirname + "/privateKey.key"
command = (
f"openssl req {password_option} -x509 -sha256 -days 15 -newkey rsa:2048 -keyout {private_key} "
f"-out {certificate} -subj '/C=EN/ST=IDF/L=London/O=Enterprise/OU=R&D Department/CN=test.com'"
)
exitcode, output = subprocess.getstatusoutput(command)
if exitcode == 0 and encrypted:
return certificate, private_key, password
elif exitcode == 0:
return certificate, private_key
def get_base64_thumbprint(filename: str):
command = f"openssl x509 -outform der -in {filename} | openssl dgst -binary -sha1 | openssl base64"
exitcode, output = subprocess.getstatusoutput(command)
if exitcode == 0:
return output
def get_thumbprint(filename):
command = (
f"openssl x509 -fingerprint -noout -in {filename} | tr -d : | cut -d '=' -f2-"
)
exitcode, output = subprocess.getstatusoutput(command)
if exitcode == 0:
return output
def get_base64_inline(filename):
with open(filename, "r") as f:
content = f.read()
return (
content.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("-----BEGIN ENCRYPTED PRIVATE KEY-----", "")
.replace("-----END ENCRYPTED PRIVATE KEY-----", "")
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\n", "")
.replace("\r", "")
)
def get_password(password: str, custom_key_identifier: str, key_id: str):
return {
"customKeyIdentifier": custom_key_identifier,
"keyId": key_id,
"endDateTime": (datetime.now() + timedelta(days=4)).isoformat(),
"startDateTime": (datetime.now() - timedelta(days=4)).isoformat(),
"secretText": password,
}
def gen_key_credentials_from_crt(filename):
return {
"customKeyIdentifier": get_base64_thumbprint(filename),
"keyId": str(uuid.uuid4()),
"type": "AsymmetricX509Cert",
"usage": "Verify",
"key": get_base64_inline(filename),
}
def gen_key_credentials_from_private_key(filename: str, custom_key_identifier: str):
# X509CertAndPassword
return {
"customKeyIdentifier": custom_key_identifier,
"keyId": str(uuid.uuid4()),
"type": "AsymmetricX509Cert",
"usage": "Sign",
"key": get_base64_inline(filename),
}
def gen_all(path: Optional[str] = None):
dir_path = os.path.dirname(os.path.realpath(__file__))
if not path:
path = ""
certificate_filename, private_key_filename, password = gen_cert_and_private_key(
dir_path + path, True
)
public_key = gen_key_credentials_from_crt(certificate_filename)
private_key = gen_key_credentials_from_private_key(
private_key_filename, public_key["customKeyIdentifier"]
)
password_cred = get_password(
password, public_key["customKeyIdentifier"], private_key["keyId"]
)
thumbprint = get_thumbprint(certificate_filename)
return {
"keyCredentials": [private_key, public_key],
"passwordCredentials": [password_cred],
"preferredTokenSigningKeyThumbprint": thumbprint,
}
if __name__ == "__main__":
data = {
"preferredSingleSignOnMode": "saml",
"notificationEmailAddresses": ["fake@test.com"],
}
cred = gen_all()
z = {**cred, **data}
print(json.dumps(z, indent=2))