Azure Graph API - Add a custom signing key - Invalid certificate: Key value is invalid certificate

Viewed 975

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))
2 Answers

The data you are posting for "usage": "Sign", "key": "MIIFHDBOBgkqhkiG9w0BBQ0wQTApB..........."

is PKCS5. The API expects this to be PKCS12 (or PFX) data base64 encoded.

For retrieve the key for "usage": "Sign, my solution:

I convert the pem to pfx :

def convert_pem_to_pfx(public_key: str, private_key: str, password: str) -> str:
    dir_path = os.path.dirname(os.path.realpath(__file__))
    command = f"openssl pkcs12 -export -out {dir_path}/data/certificate.pfx -inkey {private_key} -in {public_key} -passout pass:{password} -passin pass:{password}"
    exitcode, output = subprocess.getstatusoutput(command)
    if exitcode != 0:
        assert False
    return f"{dir_path}/data/certificate.pfx"

I extract the base64 from pfx file :

def get_base64_from_file(filename) -> str:
    with open(filename, "rb") as f:
        content = f.read()

    return base64.b64encode(content).decode('ascii')
Related