экспорт открытого ключа в формате pem из объекта x509certificate2

Я новичок в этой теме, и я запутался в различиях между открытым ключом в формате PEM и форматом CER.

Я пытаюсь экспортировать открытый ключ из объекта x509certificate2 в формате PEM в коде С#.

Насколько я понимаю, разница между сертификатом в формате cer и форматом pem заключается только в шапке и футере (если я правильно понимаю, сертификат в формате .cer в base 64 должен быть someBase64String, а в формате pem это одна и та же строка включая начальный и конечный верхний и нижний колонтитулы).

но мой вопрос для открытого ключа. пусть pubKey будет открытым ключом, экспортированным в формате .cer из объекта x509certificate2, это формат pem этого ключа, будет:

------BEGIN PUBLIC KEY-----
pubKey...
------END PUBLIC KEY------

закодировано в базе 64?

Спасибо :)


person Amit Enoch    schedule 30.12.2017    source источник


Ответы (2)


для открытого ключа. пусть pubKey будет открытым ключом, экспортированным в формате .cer из объекта x509certificate2

Говорить о «формате .cer» можно только в том случае, если у вас есть весь сертификат; и это все, что X509Certificate2 будет экспортировать. (Ну или набор сертификатов, или набор сертификатов со связанными приватными ключами).

Ничто, встроенное в .NET, не даст вам блок сертификата SubjectPublicKeyInfo в кодировке DER, который становится «ПУБЛИЧНЫМ КЛЮЧЕМ» в кодировке PEM.

Вы можете построить данные самостоятельно, если хотите. Для RSA это не так уж плохо, хотя и не совсем приятно. Формат данных определяется в https://tools.ietf.org/html/rfc3280#section-4.1:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
    algorithm            AlgorithmIdentifier,
    subjectPublicKey     BIT STRING  }

AlgorithmIdentifier  ::=  SEQUENCE  {
    algorithm               OBJECT IDENTIFIER,
    parameters              ANY DEFINED BY algorithm OPTIONAL  }

https://tools.ietf.org/html/rfc3279#section-2.3.1 описывает, как, в частности, должны быть закодированы ключи RSA:

OID rsaEncryption предназначен для использования в поле алгоритма значения типа AlgorithmIdentifier. Поле параметров ДОЛЖНО иметь тип ASN.1 NULL для этого идентификатора алгоритма.

Открытый ключ RSA ДОЛЖЕН быть закодирован с использованием RSAPublicKey типа ASN.1:

RSAPublicKey ::= SEQUENCE {
    modulus            INTEGER,    -- n
    publicExponent     INTEGER  }  -- e

Языком этих структур является ASN.1, определенный ITU X.680, и способ их кодирования в байты регулируется набором правил Distinguished Encoding Rules (DER) ITU X.690.

.NET на самом деле возвращает вам много этих частей, но вы должны их собрать:

private static string BuildPublicKeyPem(X509Certificate2 cert)
{
    byte[] algOid;

    switch (cert.GetKeyAlgorithm())
    {
        case "1.2.840.113549.1.1.1":
            algOid = new byte[] { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(cert), $"Need an OID lookup for {cert.GetKeyAlgorithm()}");
    }

    byte[] algParams = cert.GetKeyAlgorithmParameters();
    byte[] publicKey = WrapAsBitString(cert.GetPublicKey());

    byte[] algId = BuildSimpleDerSequence(algOid, algParams);
    byte[] spki = BuildSimpleDerSequence(algId, publicKey);

    return PemEncode(spki, "PUBLIC KEY");
}

private static string PemEncode(byte[] berData, string pemLabel)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("-----BEGIN ");
    builder.Append(pemLabel);
    builder.AppendLine("-----");
    builder.AppendLine(Convert.ToBase64String(berData, Base64FormattingOptions.InsertLineBreaks));
    builder.Append("-----END ");
    builder.Append(pemLabel);
    builder.AppendLine("-----");

    return builder.ToString();
}

private static byte[] BuildSimpleDerSequence(params byte[][] values)
{
    int totalLength = values.Sum(v => v.Length);
    byte[] len = EncodeDerLength(totalLength);
    int offset = 1;

    byte[] seq = new byte[totalLength + len.Length + 1];
    seq[0] = 0x30;

    Buffer.BlockCopy(len, 0, seq, offset, len.Length);
    offset += len.Length;

    foreach (byte[] value in values)
    {
        Buffer.BlockCopy(value, 0, seq, offset, value.Length);
        offset += value.Length;
    }

    return seq;
}

private static byte[] WrapAsBitString(byte[] value)
{
    byte[] len = EncodeDerLength(value.Length + 1);
    byte[] bitString = new byte[value.Length + len.Length + 2];
    bitString[0] = 0x03;
    Buffer.BlockCopy(len, 0, bitString, 1, len.Length);
    bitString[len.Length + 1] = 0x00;
    Buffer.BlockCopy(value, 0, bitString, len.Length + 2, value.Length);
    return bitString;
}

private static byte[] EncodeDerLength(int length)
{
    if (length <= 0x7F)
    {
        return new byte[] { (byte)length };
    }

    if (length <= 0xFF)
    {
        return new byte[] { 0x81, (byte)length };
    }

    if (length <= 0xFFFF)
    {
        return new byte[]
        {
            0x82,
            (byte)(length >> 8),
            (byte)length,
        };
    }

    if (length <= 0xFFFFFF)
    {
        return new byte[]
        {
            0x83,
            (byte)(length >> 16),
            (byte)(length >> 8),
            (byte)length,
        };
    }

    return new byte[]
    {
        0x84,
        (byte)(length >> 24),
        (byte)(length >> 16),
        (byte)(length >> 8),
        (byte)length,
    };
}

Ключи DSA и ECDSA имеют более сложные значения для AlgorithmIdentifier.parameters, но функция GetKeyAlgorithmParameters() X509Certificate возвращает их в правильном формате, поэтому вам просто нужно записать ключ поиска их OID (строка) и их OID (byte[]) в кодировке. значение в операторе switch.

Мои компоновщики SEQUENCE и BIT STRING определенно могут быть более эффективными (о, посмотрите на все эти бедные массивы), но этого будет достаточно для чего-то, что не критично по производительности.

Чтобы проверить свои результаты, вы можете вставить вывод в openssl rsa -pubin -text -noout, и если он напечатает что-либо, кроме ошибки, вы сделали юридически закодированную кодировку «ПУБЛИЧНОГО КЛЮЧА» для ключа RSA.

person bartonjs    schedule 31.12.2017

Начиная с .NET Core 3.0 (и .NET Standard 2.1) вы можете использовать ExportSubjectPublicKeyInfo следующим образом:

certificate.PublicKey.Key.ExportSubjectPublicKeyInfo()

Если PublicKey.Key выдает исключение (поддерживает только RSA и DSA), используйте одно из ECDsaCertificateExtensions.GetECDsaPublicKey, RSACertificateExtensions.GetRSAPublicKey, DSACertificateExtensions.GetDSAPublicKey