Azure Graph API - 添加自定义签名密钥 - 证书无效:密钥值是无效证书

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

当我想按照教程进行操作时: https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-saml-sso-configure-api#add-a-custom-signing-key.

我一直在更新服务以添加我的私钥和证书。 我不想点击 azure 界面,我想通过 Graph API 做所有事情(它用于自动测试)。我不使用 Windows,因此我的脚本不使用 Powershell,我在 Linux.

上使用 Python 和 OpenSSL

'Request_BadRequest','Invalid certificate: Key value is invalid certificate',状态:400

发送的数据:

"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"
  ]
}

完成的测试:

生成数据的代码 (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))

您发布的数据 “用法”:“签名”, “键”:“MIIFHDBOBgkqhkiG9w0BBQ0wQTApB……”

是 PKCS5。 API 期望这是 PKCS12(或 PFX)数据库 base64 编码。

获取“用法”的密钥:“签名,我的解决方案:

我将 pem 转换为 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"

我从 pfx 文件中提取 base64 :

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

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