Skip Ribbon Commands
Skip to main content

Ondrej Sevecek's Blog

:

Engineering and troubleshooting by Directory Master!
Ondrej Sevecek's Blog > Posts > Digitální podpis obsahu do formátu PKCS#7 a zjištění CRL a OCSP cest
duben 26
Digitální podpis obsahu do formátu PKCS#7 a zjištění CRL a OCSP cest

Zrovna řeším dyzajn jednoho krypto-systému pro digitální podpis dokumentů pomocí kvalifikovaných certifikátů a zjišťuju, jaké se k tomu dají použít knihovny. Cílem je vzít data, podepsat je pomocí (ideálně .NET framework) nějakých funkcí do formátu PKCS#7.

Standard PKCS#7 je používán ve všech aplikacích, jako jsou podpisy XML (XML Advanced Electronic Signatures - XAdES), obecné podpisy (​CMS Advanced Electronic Signatures - CAdES), nebo podpisy PDF podle ETSI (PAdES), SAFE-BioPharma, Microsoft Office, nebo třeba Authenticode apod. Jenže kvůli long term validation (LTV) je také potřeba sehnat a ručně zazálohovat všechna CRL a případně OCSP odpovědi ode všech certifikátů autorit i koncového podepisovacího certifikátu. Co se týče problémů časových razítek, tak to až někdy příště.

Formát PKCS#7 má dvě základní možnosti jak vyrobit podpis. Buď může obalit i zdrojová data (envelope), nebo prostě vytvořit jen samostatně stojící podpisový balíček (detached signature). Detached signature balíček je přirozeně vhodnější, protože podepisovaná data mohou ležet samostatně. Není potřeba je ani modifikovat, i když podpis realizujete později.

Základním vstupním bodem našeho snažení jsou jen dvě věci, které chceme vyzvednout od uživatele. Data, která chce podepsat, v mém případě například jednoduchý text. Druhá věc je koncový (uživatelský) certifikát, kterým si přeje data podepsat. Nic dalšího by uživatel neměl zadávat. Představme si to tak, že si prostě jen vybere v nějakém rozhraní certifikát a zadá textík, nebo dokument, který chce podepsat.

K podepisování používám .NET framework knihovnu System.Security.Cryptography. Takhle knihovna umí do PKCS#7 balíčku vložit jak podepisovací certifikát, tak i celou jeho certifikátovou cestu. Tedy samo to tam vloží všechny certifikáty autorit včetně kořenové (volba WholeChain). To je pěkné, ale zase to ve výsledku žere zbytečně moc místa (výsledek bude cca 3 kB). Dá se to ale vypnout (volba None) a potom zbude jen čistý PKCS#7 podpis, který má něco okolo 500 kB.

Paráda, abychom nemuseli hledat podepisovací certifikát ani certifikáty autorit sami, prostě uděláme podpis i s certifikáty a vybereme si je z toho PKCS#7 výsledku. A rovnou to uděláme ještě jednou bez certifikátů a budeme mít nejmenší možný PKCS#7 podpis k uložení do databáze.

Diskuze: je možné ukládat data, certifikáty i jejich CRL, nebo OCSP samostatně? Ano. Všechno je přece digitálně podepsáno. Pokud by byly pochybnosti o tom, jestli byl daný certifikát použit k podepsání nějakého textu, nebo jestli příslušné CRL patří dané certifikační autoritě, stačí prostě ověřit jejich signatury.

Následuje vzorový prográmek v PowerShell, který to všechno používá. Krása PowerShellu je v tom, že to prostě používá .NET a přitom se to nemusí kompilovat a je to krásně jednoduché:

$textToSign = 'ahoj'

# Should we envelope the source data into the PKCS#7 package
# or should we create the detached signature, which does not
# contain the source data instead.
$detachedSignature = $false

# Should the resulting PKCS#7 package include the signing certificate?
# None - include no certificate (binary result will be about 500 B)
# EndCertOnly - include only the signing certificate itself
# ExcludeRoot - all certificates in the chain excluding the root
# WholeChain - all certificates in the chain including the root (binary
#              result will be about 3 kB)
$includeOption = 'WholeChain'

# Get first System.Security.Cryptography.X509Certificates.X509Certificate2 certificate
# which contains a private key. We are going for the -CodeSigningCert here
# but any more suitable certificate can be selected according to you needs
$signingCert = dir Cert:\CurrentUser\My -CodeSigningCert | ? { $_.PrivateKey -ne $null } | Select -First 1



Write-Host ('Selected certificate subject: {0}' -f $signingCert.Subject)
Write-Host ('Selected certificate thumbprint: {0}' -f $signingCert.Thumbprint)
Write-Host ('Selected certificate CSP: {0}' -f $signingCert.PrivateKey.CspKeyContainerInfo.ProviderName)


[System.Reflection.Assembly]::LoadWithPartialName('System.Security') | Out-Null


# We will need the CryptGetObjectUrl function later, so go straight
# and define it here

$cryptGetObjectURLdef = @"

public const int URL_OID_CERTIFICATE_ISSUER = 1;
public const int URL_OID_CERTIFICATE_CRL_DIST_POINT = 2;
public const int URL_OID_CTL_ISSUER = 3;
public const int URL_OID_CTL_NEXT_UPDATE = 4;
public const int URL_OID_CRL_ISSUER = 5;
public const int URL_OID_CERTIFICATE_FRESHEST_CRL = 6;
public const int URL_OID_CRL_FRESHEST_CRL = 7;
public const int URL_OID_CROSS_CERT_DIST_POINT = 8;
public const int URL_OID_CERTIFICATE_OCSP = 9;
public const int URL_OID_CERTIFICATE_OCSP_AND_CRL_DIST_POINT = 10;
public const int URL_OID_CERTIFICATE_CRL_DIST_POINT_AND_OCSP = 11;
public const int URL_OID_CROSS_CERT_SUBJECT_INFO_ACCESS = 12;
public const int URL_OID_CERTIFICATE_ONLY_OCSP = 13;

public const int CRYPT_GET_URL_FROM_PROPERTY = 1;
public const int CRYPT_GET_URL_FROM_EXTENSION = 2;
public const int CRYPT_GET_URL_FROM_UNAUTH_ATTRIBUTE = 3;
public const int CRYPT_GET_URL_FROM_AUTH_ATTRIBUTE = 8;

[DllImport("cryptnet.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool CryptGetObjectUrl(
    int pszUrlOid,
    IntPtr pvPara,
    int dwFlags,
    byte[] pUrlArray,
    ref int pcbUrlArray,
    IntPtr pUrlInfo,
    ref int pcbUrlInfo,
    int pvReserved
);
"@

Add-Type -MemberDefinition $cryptGetObjectURLdef -Name CryptNetDll -NameSpace Sevecek

function global:Get-CertificateUrl ([Security.Cryptography.X509Certificates.X509Certificate2] $certificate, [int] $urlType)
# The function wraps CryptGetObjectUrl Win32 API to obtain URL paths from a certificate
# You can obtain CRL (CDP), AIA as well as OCSP paths:
#   CRL: $urlType = URL_OID_CERTIFICATE_CRL_DIST_POINT
#   AIA: $urlType = URL_OID_CERTIFICATE_ISSUER
#   OCSP: $urlType = URL_OID_CERTIFICATE_ONLY_OCSP
{
  [string[]] $urls = @()

  $pvPara = $Cert.Handle

  $pcbUrlArray = 0
  $pcbUrlInfo = 0

  # call the function first with zero-length buffers to get their real sizes
  $apiRes = [Sevecek.CryptNetDll]::CryptGetObjectUrl(
    $urlType,
    $certificate.Handle,
    [Sevecek.CryptNetDll]::CRYPT_GET_URL_FROM_EXTENSION,
    $null,
    [ref] $pcbUrlArray,
    0,
    [ref] $pcbUrlInfo,
    0)

  if ($apiRes -eq $true) {

    $pUrlArray = New-Object byte[] -ArgumentList $pcbUrlArray
    $pUrlInfo = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbUrlInfo)

    # call the function first with zero-length buffers to get their real sizes
    $apiRes = [Sevecek.CryptNetDll]::CryptGetObjectUrl(
      $urlType,
      $certificate.Handle,
      [Sevecek.CryptNetDll]::CRYPT_GET_URL_FROM_EXTENSION,
      $pUrlArray,
      [ref] $pcbUrlArray,
      $pUrlInfo,
      [ref]$pcbUrlInfo,
      0)

    if ($apiRes -eq $true) {

       # Note that the pUrlArray is actually an CRYPT_URL_ARRAY structure
       # which contain UNICODE encoded URLs. So we need to convert it into string array
       # the very first field is number of URLs. It is either 4 bytes on 32-bit systems
       # while it is 8 bytes on 64-bit systems.
       # The $pUrlArray may look like this on 64-bit system, for example:
       #      02-00-00-00-00-00-00-00
       #      08-E2-57-24-AD-00-00-00
       #      18-E2-57-24-AD-00-00-00
       #      78-E3-57-24-AD-00-00-00
       #      6C-00 l
       #      64-00 d
       #      61-00 a
       #      70-00 p:///...
       #      00-00
       #      68-00 http://...
       #      00-00
       # There are always pointers to the starts of the strings which are \0 terminated

       $is64bit = (Get-WmiObject Win32_OperatingSystem | Select-Object -ExpandProperty OSArchitecture) -eq '64-bit'

       if ($is64bit) {

         $urlCount = [BitConverter]::ToInt64($pUrlArray[0..7], 0)
 
         # and get index of the first character of the first URL
         $firstUrlChar = ($urlCount + 1) * 8 + 8

       } else {

         $urlCount = [BitConverter]::ToInt32($pUrlArray[0..3], 0)
         $firstUrlChar = ($urlCount + 1) * 4 + 4
       }

       for ($i = 0; $i -lt $urlCount; $i ++)
       {
          $lastUrlChar = $firstUrlChar
          while (-not (($pUrlArray[$lastUrlChar] -eq 0) -and ($pUrlArray[$lastUrlChar + 1] -eq 0))) { $lastUrlChar ++ }
          
          $oneUrl = [System.Text.Encoding]::Unicode.GetString($pUrlArray[$firstUrlChar..($lastUrlChar)])
          $urls += $oneUrl

          $firstUrlChar = $lastUrlChar + 3
       }
    }
  }

  return $urls
}


[byte[]] $bytesToSign = [System.Text.ASCIIEncoding]::ASCII.GetBytes($textToSign)

$cntInfo = New-Object System.Security.Cryptography.Pkcs.ContentInfo (, $bytesToSign)


$signedMsg = New-Object System.Security.Cryptography.Pkcs.SignedCms ($cntInfo, $detachedSignature)
$signer = New-Object System.Security.Cryptography.Pkcs.CmsSigner ($signingCert)
$signer.IncludeOption = $includeOption

$signedMsg.ComputeSignature($signer)

$signedResult = $signedMsg.Encode()


Write-Host ('Size of the binary encoded PKCS#7 package: {0}' -f ($signedResult.Count))
Write-Host ('Binary PKCS#7 package: {0}' -f ([BitConverter]::ToString($signedResult)))
Write-Host ('Certificates inside the package: {0}' -f ($signedMsg.Certificates | Out-String))


# Now get all CRL/OCSP paths of all the certificates included
$signedMsg.Certificates | % {

  $oneCert = $_

  Write-Host ('Certificate subject: {0}' -f $oneCert.Subject)
  Write-Host ('Certificate thumbprint: {0}' -f $oneCert.Thumbprint)

  # AIA extension - Authority Information Access contains AIA records
  # and possibly even the OCSP URL
  $oneAIA = $oneCert.Extensions | ? { $_.OID.Value -eq '1.3.6.1.5.5.7.1.1' }
  
  # CDP extension - CRL Distribution Points
  $oneCDP = $oneCert.Extensions | ? { $_.OID.Value -eq '2.5.29.31' }

  # Note that the .Format method returns regionally formated and localized strings
  # so the only thing we rely on here is a wildcard search string. 

  $oneAIAText = ''
  if ($oneAIA -ne $null) { $oneAIAText = $oneAIA.Format($true) }

  #Write-Host 'Formated AIA contents:'
  #Write-Host $oneAIAText

  $oneCDPText = ''
  if ($oneCDP -ne $null) { $oneCDPText = $oneCDP.Format($true) }

  #Write-Host 'Formated CDP contents:'
  #Write-Host $oneCDPText

  # Or we can extract the precise URL values with CryptGetObjectUrl Win32API function
  Write-Host 'Raw AIA contents:'
  Get-CertificateUrl $oneCert ([Sevecek.CryptNetDll]::URL_OID_CERTIFICATE_ISSUER)

  Write-Host 'Raw CDP contents:'
  Get-CertificateUrl $oneCert ([Sevecek.CryptNetDll]::URL_OID_CERTIFICATE_CRL_DIST_POINT)

  Write-Host 'Raw OCSP contents:'
  Get-CertificateUrl $oneCert ([Sevecek.CryptNetDll]::URL_OID_CERTIFICATE_ONLY_OCSP)
}

A teď ještě nějaké poznámky ke stahování CRL a/nebo OCSP

Nejprve otázka - pokud si podepisuju dokumenty, musím si k tomu stahovat CRL, nebo OCSP? Já tvrdím že ne. Jde přece o to, na kom leží důkazní povinnost. Jestliže já podepisuju svým certifikátem, dělám to proto, že si sám věřím. Až mi někdo bude chtít dokázat, že jsem to podepsal neprávem, tak ať přijde s CRL, kde je vidět, že můj certifikát byl už tehdy neplatný.

Otázka je, jestli tohle ví soudy, znalci a policie. Takže já bych asi raději stahoval CRL a/nebo OCSP pro jistotu, abych měl sám svoje protidůkazy.

Ale to není ve skutečnosti žádná sranda. Pro jeden uživatelský certifikát je potřeba zálohovat CRL. Horší na tom je, že je potřeba zálohovat zřejmě všechna CRL a OCSP odpovědi od okamžiku, kdy se podpis uskutečnil až do konce platnosti toho certifikátu jeho vydávající autority. Cože?

No ano, v okamžiku podpisu bylo platné nějaké CRL/OSCP. Tohle CRL platí vždycky nějakou dobu, řekněme několik dnů. I když zrovna máte platné CRL v ruce, je klidně možné, že jeho certifikační autorita (CA) už stihla certifikát zneplatnit, ale nové CRL se bude vydávat až později. To by znamenalo nutnost stáhnout ještě alespoň to další CRL, že?

Dobře, jenže to není všechno. Co když by se stalo, že někomu ukradnou certifikát. On to nahlásí až za pár týdnů, až si toho všimne. To by ho normálně ta autorita zneplatnila až později. Jenže žijeme v lidském světě. Co když to policie vyšetří a soud rozhodne, že se ten certifikát musí zneplatnit s dřívějším datem? Tak ho autorita sice dá na svoje CRL později, ale s dřívějším datem.

Musíte mít tedy všechna CRL od dané certifikační autority až do doby, kdy tahle certifikační autorita ukončí svoji činnost. To je pééklo, že?

Nestačilo by stahovat ta další CRL už jen jednou za čas? Vždyť ty neplatné certifikáty na něm budou celou dobu, ne? Ne. Zase je možné, že certifikační autorita bude přinucena nějaký certifikát znovu uvést v platnost (neoprávněné zneplatnění apod.). Pokud chcete být chránění i proti takové situaci, musíte mít celý řetěz CRL od prvního použití daného certifikátu.

A ještě jedna věc. Někteří špekulanti by se snažili ušetřit a stahovat jen jedno CRL pro každou certifikační autoritu. Pozor! Technicky je klidně možné, že dva různé certifikáty od stejné CA budou odkazovat na jiné CRL!

Delta CRL

Aby to nebylo ještě i tak moc jednoduché, existují dva typy CRL. Certifikáty přímo odkazují na tzv. base CRL (základní CRL). Některé autority ale tohle base CRL vydávají jen jednou za čas, protože může být docela velké. Mezitím vydávají přírustkové CRL - tzv. delta CRL.

Adresa delta CRL ke stažení je ale až v těch base CRL. Takže když máte certifikát, stáhnete base CRL, jakmile máte i base CRL, musíte v něm zkontrolovat (rozšíření Freshest CRL = OID 2.5.29.46), jestli není potřeba stáhnout ještě delta CRL. Ale to už je zase trošku jiná pohádka.

A pohádka číslo tři je o stahování OCSP :-) protože to není jen tak nějaký soubor. Musíte udělat HTTP POST a správně ho naformátovat do ASN.1.

Comments

Re: Digitální podpis obsahu do formátu PKCS#7 a zjištění CRL a OCSP cest

Dobrý den, Ondro,

CRL (či OCSP) je nutné archivovat i kvůli nešikovně formulovanému § 5 odst. 2 zákona o elektronickém podpisu. Ten totiž vykročil ze základních procesně právních zásad "Kdo se něčeho domáhá, má povinnost tvrzení a povinnost důkazní." a "Nedokazuje se to, co není.". Trochu nesmyslně přenáší důkazní břemeno na autora podpisu, aby dokázal, že ten, kdo po něm žádá náhradu své škody, si před vznikem škody špatně ověřil platnost podpisu. U značek (§ 5a odst. 2) je to mimochodem ještě horší, tam má autor objektivní odpovědnost za náhodnou ztrátu klíče, nemusí se jednat ani o nevědomou nedbalost (!). Jelikož autor nemá zpravidla jak dokázat, že někdo cizí udělal chybu při ověření podpisu, má jedinou možnost - a contrario dokázat, že on své povinnosti nezanedbal, tedy nechat si razítkovat kompletní historii CRLs (OCSP responses). Takto je bohužel nutné oba odst. 2 vykládat.

Zajímavé je, že evropská směrnice takovou formulaci vůbec nevyžadovala. Na druhou stranu, zákon nás jen nešikovnou formulací nabádá k celkem logickému závěru: že archivace důkazů o nekompromitaci je dobrá praxe každého vzorného autora podpisů. :)

K Vašemu PowerShellu: Respekt! (VS a C# už při psaní pomůže a leccos odchytí, nemluvě o dalším VS debuggingu. S PS ISE tedy klobouk dolů.)
Jan Lenoch on 27.6.2013 15:46

Add Comment

Title


Pole Title nemusíte vyplňovat, doplní se to samo na stejnou hodnotu jako je nadpis článku.

Author *


Pole Author nesmí být stejné jako pole Title! Mám to tu jako ochranu proti spamu. Roboti to nevyplní dobře :-)

Body *


Type number two as digit *


Semhle vyplňte číslici dvě. Předchozí antispemové pole nefunguje úplně dokonale, zdá se, že jsou i spamery, které pochopily, že je občas potřeba vyplnit autora :-)

Email


Emailová adresa, pokud na ni chcete ode mě dostat odpověď. Nikdo jiný než já vaši emailovou adresu neuvidí.

Attachments