Mic utilitar C# pentru extragerea conținutului relevant dintr-un fișier PEM

Formatul PEM (Privacy-Enhanced Mail) este o modalitate bazată pe vechea filozofie antică textus chiorensis de-a împacheta date binare – de regulă chei criptografice, certificate X.509 sau alte asemenea artefacte criptografice – într-un fișier ușor de transmis prin canale de comunicație ce suportă exclusiv codificări ASCII.

Într-adevăr

Într-adevăr

Procedura presupune codificarea Băse64 a conținutului binar reprezentând artefactul în format ASN.1 serializat DER (Distinguished Encoding Rules) și-ncadrarea rezultatului obținut între două linii delimitatoare cu o sintaxă bine definită, cum ar fi, în cazul certificatelor (mai jos, un exemplu complet):

– drep antet, marcând începutul mesajului ––BEGIN CERTIFICATE––
– respectiv ––END CERTIFICATE–– pentru, să zicem, linia de subsol, marcând, ca atare încheierea mesajului relevant.

-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----

Desigur, certificatul nu este singurul bun digital ce poate fi transmis ca atare, ci și: chei publice, chei private, cereri de emitere a certificatelor (certificate requests), liste de revocare a certificatelor (certificate revocation lists) etc.

Consumarea unui asemenea format este simplă, oferindu-vă cu această ocazie și un mic utilitar C# compus din două clase, suficient de inteligent încât să ignore datele dinainte de antet și de după linia de subsol. De asemenea, tratează corect inclusiv plasarea celor trei elemente (antet – mesaj – subsol) pe o singură linie.

public class GenericPEMParser : IDisposable
{
    private Stream mWorkStream;

    private bool mIsDisposed = false;

    private string mHeader;

    private string mFooter;

    private bool mOwnsStream = false;

    public GenericPEMParser( Stream workStream, 
        string header, 
        string footer, 
        bool ownStream )
    {
        if (string.IsNullOrEmpty( header ))
            throw new ArgumentNullException( nameof( header ) );

        if (string.IsNullOrEmpty( footer ))
            throw new ArgumentNullException( nameof( footer ) );

        mWorkStream = workStream
            ?? throw new ArgumentNullException( nameof( workStream ) );

        mHeader = header;
        mFooter = footer;
        mOwnsStream = ownStream;
    }

    public static GenericPEMParser PublicKeyParser( Stream workStream, 
        bool ownStream )
    {
        return new GenericPEMParser( workStream,
            PEMTags.BeginPublicKey,
            PEMTags.EndPublicKey,
            ownStream );
    }

    public static GenericPEMParser CertificateParser( Stream workStream, 
        bool ownStream )
    {
        return new GenericPEMParser( workStream,
            PEMTags.BeginCertificate,
            PEMTags.EndCertificate,
            ownStream );
    }

    public byte [] ReadObject()
    {
        StringBuilder buffer =
            new StringBuilder();

        mWorkStream.Seek( 0, SeekOrigin.Begin );

        using (StreamReader rdr = new StreamReader( mWorkStream,
            Encoding.ASCII,
            detectEncodingFromByteOrderMarks: true,
            bufferSize: 256,
            leaveOpen: true ))
        {
            string line = rdr.ReadLine();
            bool startCollecting = false;

            StringComparison comparison = StringComparison
                .InvariantCultureIgnoreCase;

            while (line != null)
            {
                line = line.Trim();

                startCollecting = startCollecting
                    || line.StartsWith( mHeader, comparison );

                if (line.EndsWith( mFooter, comparison ))
                    break;

                if (startCollecting)
                {
                    line = line.Replace( mHeader, "", comparison );
                    line = line.Replace( mFooter, "", comparison );

                    if (!string.IsNullOrWhiteSpace( line ))
                        buffer.Append( line );
                }

                line = rdr.ReadLine();
            }
        }

        return Convert.FromBase64String( buffer.ToString() );
    }

    protected virtual void Dispose( bool disposing )
    {
        if (!mIsDisposed)
        {
            if (disposing)
            {
                if (mOwnsStream)
                    mWorkStream?.Dispose();
                mWorkStream = null;
            }

            mIsDisposed = true;
        }
    }

    public void Dispose()
    {
        Dispose( true );
    }
}

Parametrul ownStream determină dacă în metoda .Dispose() se gestionează și fluxul de date workStream (valoarea true va determina apelarea .Dispose() și pentru workStream; valorea false îl va lăsa-n boii lui și pe răspunderea apelantului).

În continuare, clasa PEMTags, unde-am definit marcatorii relevanți (pentru mine) ai antetului, respectiv subsolului:

public static class PEMTags
{
    public const string BeginPublicKey
        = "-----BEGIN PUBLIC KEY-----";

    public const string EndPublicKey
        = "-----END PUBLIC KEY-----";

    public const string BeginCertificate
        = "-----BEGIN CERTIFICATE-----";

    public const string EndCertificate
        = "-----END CERTIFICATE-----";
}

În sfârșit, un exemplu de utilizare ce va tipări datele obținute în format HEX-ASCII:

public static void RunPEMTryOuts()
{
    byte [] certPemData = Encoding.ASCII
        .GetBytes( GetPEMCertificateText() );
    
    using (MemoryStream memoryStream = 
        new MemoryStream( certPemData ))

    using (GenericPEMParser parser = 
        GenericPEMParser.CertificateParser( memoryStream, 
            ownStream: false ))
    {
        byte [] certData = parser.ReadObject();
        
        string certHexAscii = BitConverter
            .ToString( certData )
            .Replace( "-", "" );
            
        Console.WriteLine( certHexAscii );
    }
}

Dacă veți copia ce se afișează pe ecran și ulterior veți introduce aici, veți putea vedea și structura ASN.1 a certificatului:

Exemplu certificat deserializat

Exemplu certificat deserializat