マイナンバーカードの秘密鍵からCSR作成、独自CAによる署名用証明書作成

マイナンバーカードに格納されている署名用証明書を使って Windowsプログラムに署名する方法が紹介されていました。


https://qiita.com/binzume/items/3c09a7f2434e6c4478d9

記事では Linux で署名していましたので、Windows でも署名できることを検証してみたところ、同じように署名することができました。

しかし、記事の後半の　”色々試行錯誤しましたが，マイナンバーカードでWindowsアプリケーションにコード署名をして嬉しい場面は少なそうです”　という感想には同感です。

特に、署名用証明書に記載されている個人情報が公開されてしまうのは問題かもしれません。

そこで、本記事ではマイナンバーカードに格納されている署名用証明書の代わりに、マイナンバーカードの秘密鍵から

1. CSR作成

そして、

2. ルートCA作成
3. 作成したルートCAで CSRに署名、署名用証明書を作成

する方法を紹介します。

このようにして作成した署名用証明書でコード署名することで、公開される情報をコントールできます。

ルートCAの証明書をコード署名されたコンピュータの信頼されたルート証明機関に登録すると、コード署名は検証されます。

A. CSR作成
OpenSSL で CSR を作成します。マイナンバーカード内の秘密鍵から作成しなければなりませんので、pkcs11 engine (https://github.com/OpenSC/libp11)を使うことにします。

Windows用にビルド(pkcs11.dll)できたら、openssl のコンフィグファイルに OpenSC の pkcs11 モジュールとエンジンモジュールを登録します。

[openssl_init]
engines=engine_section

[engine_section]
pkcs11=pkcs11_section

[pkcs11_section]
engine_id=pkcs11
dynamic_path=".\\pkcs11.dll"
MODULE_PATH=".\\opensc-pkcs11.dll"
init=0

この登録により OpenSC の pkcs11モジュールでマイナカードの秘密鍵にアクセスできるようになります。

openssl.exe とコンフィグ（openssl.cnf )は同じフォルダに置かれているとして、そのフォルダでターミナルを開き、以下コマンドを実行すると CSRファイル( my.csr ) が作成されます。

-subj は任意のものに変更してください。ここで指定するものがコード署名したときに公開されます。

 ./openssl req -config openssl.cnf -engine pkcs11 -nodes -new -key 1:2 -keyform engine -subj "/CN=xxx/OU=xxx/O=yyy/ST=xxx/L=xxxx/C=JP"　-out my.csr

-key 1:2 を指定していますので、仮想スロット1 の ID 2 の秘密鍵にアクセスします。


B. ルートCA作成
生成した秘密鍵からCSRを作成して、自身の秘密鍵で署名した自己署名証明書を作成します。また、CAに必要なディレクトリを作成します。手作業では面倒ですので、用意したPowerShellスクリプトで行います。

使い方：

./createCA.ps1 -ca myCA

createCA.ps1 と同じフォルダに myCA というフォルダ内にルートCAを作成します。

```createCA.ps1

Param(
    $dir, $upperCA, $days
)

function Resolve-FullPath{
    [cmdletbinding()]
    param
    (
        [Parameter(
            Mandatory=$true,
            Position=0,
            ValueFromPipeline=$true)]
        [string] $path
    )
     
    $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
}

$createRootCA=$false

if( $dir -eq $null)
{
    $scriptName =$MyInvocation.InvocationName
    $scriptName = [System.IO.Path]::GetFileName($scriptName)

    echo ""
    echo "usage: ${scriptName} -dir (folder name for CA to be created) -upperCA (upperCA folder) -days (valid days Default 9999)"
    echo "  To create root CA, do not supply -upperCA option"
    echo ""
    return
}

$createRootCA = $false

if( $upperCA -eq $null)
{
    $createRootCA=$true
    echo "Creating a root CA in $dir"
    $caTypeMsg = "Root CA"
}
else
{
    echo "Creating an Intermediate CA in $dir"
    $caTypeMsg = "Intermedidate CA"
}

if( $days -eq $null)
{
    $days = 9999
} 

if( $days -lt 1)
{
    $days = 1
}

# Convert to absolute Paths
$dir = Resolve-FullPath($dir);

if( $createRootCA -eq $false )
{
    $upperCA = Resolve-FullPath($upperCA);

    #see if there is ca folder
    if( -not (Test-Path -Path ${upperCA} -PathType Container))
    {
        echo ""
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo "UpperCA folder not found!"
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo ""
        return
    }

    #see if there is cakey.pem in "private" folder in upperCA folder
    if( -not (Test-Path -Path "${upperCA}\private\cakey.pem" -PathType Leaf))
    {
        echo ""
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo "UpperCA private key not found!"
        echo "upperCA folder must have cakey.pem in [private] folder"
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo ""
        return
    }

    #see if there is cacert.pem in the parent folder
    if( -not (Test-Path -Path "${upperCA}\cacert.pem" -PathType Leaf))
    {
        echo ""
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo "UpperCA certificate not found!"
        echo "There must be cacert.pem in upperCA folder"
        echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        echo ""
        return
    }
}

$title    = 'Confirm'
$choices  = '&Yes', '&No'

#CA folder must be new
if( Test-Path -Path ${dir} -PathType Container)
{
    echo ""
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo " Specified ${caTypeMsg} folder already exists"
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo ""
    $question = "Do you want to use the existing folder ${dir}?"
}
else {
    $question = "Do you want to create ${caTypeMsg} in ${dir}?"
}

$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 1) {return}

if( -not (Test-Path -Path ${dir} -PathType Container))
{
    New-Item $dir -ItemType Directory > $null
}

$curDir = Get-Location
Set-Location $dir

try {   

    if( -not (Test-Path -Path 'certs' -PathType Container))
    {
        New-Item "certs" -ItemType Directory > $null
    }

    if( -not (Test-Path -Path 'crl' -PathType Container))
    {
        New-Item "crl" -ItemType Directory > $null
    }

    if( -not (Test-Path -Path 'newcerts' -PathType Container))
    {
        New-Item "newcerts" -ItemType Directory > $null
    }

    if( -not (Test-Path -Path 'private' -PathType Container))
    {
        New-Item "private" -ItemType Directory > $null
    }

    if( -not (Test-Path -Path 'csr' -PathType Container))
    {
        New-Item "csr" -ItemType Directory > $null
    }

    if( -not (Test-Path -Path 'index.txt' -PathType Leaf))
    {
        New-Item index.txt -ItemType File > $null
    }

    if( -not (Test-Path -Path 'crlnumber' -PathType Leaf))
    {
        New-Item crlnumber -ItemType File  > $null
    }

    if( -not (Test-Path -Path 'serial' -PathType Leaf))
    {
        New-Item serial -ItemType File -Value "1000" > $null
    }

    echo ""
    Echo "Creating ${caTypeMsg} private key."
    $pwd_string = Read-Host "Enter a password to protect the key" -AsSecureString

    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd_string)
    $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

    #create intermedicate CA private key
    if( $UnsecurePassword -eq "")
    {
        $arg = "genrsa -out `"${dir}\private\cakey.pem`" 2048"
        start-process -Wait -FilePath "${curDir}\openssl.exe" -ArgumentList $arg
    }
    else
    {
        $arg = "genrsa -aes256 -out `"${dir}\private\cakey.pem`" -passout pass:$UnsecurePassword 2048"
        start-process -Wait -FilePath "${curDir}\openssl.exe" -ArgumentList $arg
    }

    Set-Location $curDir

    $country = Read-Host "Country(2 chars)" 
    $state = Read-Host "State or Province"
    $city = Read-Host "Locality (city)"
    $org = Read-Host "Organization Name"
    $org_unit = Read-Host "Organization Unit"
    $cn = Read-Host "Common Name"

    Set-Location $dir

    if( $createRootCA -eq $true)
    {
        $arg = "req -config `"${curDir}\openssl.cnf`" -key `"${dir}\private\cakey.pem`" -passin pass:$UnsecurePassword -new -x509 -days $days -sha256 -extensions v3_ca -out `"${dir}\cacert.pem`" -subj `"/CN=$cn/C=$country/ST=$state/L=$city/O=$org/OU=$org_unit`""
        start-process -Wait -FilePath "${curDir}\openssl.exe" -ArgumentList $arg
    }
    else
    {

        #create CSR
        start-process -Wait -FilePath "${curDir}\openssl.exe" -ArgumentList "req -config `"${curDir}\openssl.cnf`" -new -sha256 -extensions v3_intermediate_ca -key `"${dir}\private\cakey.pem`" -passin pass:$UnsecurePassword -out intermediate.csr.pem -subj `"/CN=$cn/C=$country/ST=$state/L=$city/O=$org/OU=$org_unit`""

        #upper CA signs intermedicate CA's CSR
        Set-Location $upperCA

        echo ""
        $pwd_string = Read-Host "UpperCA Private Key Password" -AsSecureString
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd_string)
        $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

        $arg = "ca -config ${curDir}\openssl.cnf -passin pass:$UnsecurePassword -extensions v3_intermediate_ca -days $days -notext -md sha256 -in `"${dir}\intermediate.csr.pem`" -out `"${dir}\cacert.pem`" -notext -batch"
        start-process -Wait -FilePath "${curDir}\openssl.exe" -ArgumentList $arg

        #go to intermedicate CA's folder
        Set-Location $dir
    }

    #print private key and certificate
 #   clear

    echo ""
    echo "${caTypeMsg} Private Key"
    dir ${dir}\private\cakey.pem

    echo ""
    echo "${caTypeMsg} Certificate"
    dir ${dir}\cacert.pem

    echo ""
    echo "Current daytime"
    Get-Date -Format G
}
finally
{
    Set-Location $curDir
}
```

C. 作成したルートCAで CSRを署名、署名用証明書を作成
CSRの署名も手作業では面倒なため、こちらもPowerShellスクリプトで行います。スクリプトを実行する前にOpenssl のコンフィグに以下のセクションを追加してください。

```
[ client_cert]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
nsCertType                      = client
extendedKeyUsage = clientAuth
nsComment                       = "OpenSSL Generated Certificate"

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer


[ server_cert ]
basicConstraints=CA:FALSE
keyUsage =  digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
nsCertType                      = server
extendedKeyUsage = serverAuth
nsComment                       = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ codesign_cert ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature
nsCertType                      = objsign
extendedKeyUsage = codeSigning
nsComment                       = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ v3_intermediate_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical,CA:true, pathlen:0

```



>.\sign.ps1 -ca myCA -in my.csr -certType codesign -out mycodesign.crt -days 3650

ルートCA myCA で my.csr を署名、コード署名用証明書mycodesign.crt を出力。証明書有効期限は 3650日


```sign.ps1
Param(
    $ca, $certType, $in, $out, $days
)

#https://gist.github.com/sayedihashimi/02e98613efcb7280d706
function Resolve-FullPath{
    [cmdletbinding()]
    param
    (
        [Parameter(
            Mandatory=$true,
            Position=0,
            ValueFromPipeline=$true)]
        [string] $path
    )
     
    $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
}

function getUniqueFilename
{
    Param(
        $out )

    if( -not(Test-Path -Path $out -PathType Leaf ) )
    {
        return($out)
    }

    $outPathPart = Split-Path $out -Parent
    $outFilePart = [System.IO.Path]::GetFileNameWithoutExtension($out)
    $outExt = [System.IO.Path]::GetExtension($out)

    $NewName=""
    $i = 1
    do
    {
        $NewName = "${outPathPart}\${outFilePart}({0:0})${outExt}" -f $i
        $i++
    }while (Test-Path -Path $NewName -PathType Leaf)

    return($NewName)
}


function signCSR
{
    Param(
    $ca, $certType, $csr, $crt, $days )

    $crt = getUniqueFilename $crt

    echo (-join( "Signing:",$csr))
    echo (-join("        --> ",$crt))

    $extension=""

    if( $certType -eq "client")
    {
        $extension = "-extensions client_cert"
    }
    elseif( $certType -eq "server")
    {
        $extension = "-extensions server_cert"
    }
    elseif( $certType -eq "codesign")
    {
        $extension = "-extensions codesign_cert"
    }

    $arg = "ca -config `"${PSSCriptRoot}\openssl.cnf`" -in `"${csr}`" -cert `"${ca}\cacert.pem`" -keyfile `"${ca}\private\cakey.pem`" -passin pass:$UnsecurePassword $extension -out `"${crt}`" -notext -batch -days ${days}"

    start-process -Wait -FilePath "`"${PSSCriptRoot}\openssl.exe`"" -ArgumentList $arg   

    # if $crt is created but its size is 0, delete $crt
    if( Test-Path -Path $crt -PathType Leaf)
    {
        $filesize = (Get-ChildItem $crt).Length

        if( $filesize -eq 0)
        {
            Remove-Item $crt
        }
    }

    # when $crt is not found, signing has failed
    if( -not (Test-Path -Path $crt -PathType Leaf))
    {
        echo "xxx Sign error xxxx"
    }
    else 
    {
        echo " *** Sign Ok ***"
    }
    echo ""

}

if( ($ca -eq $null) -or ($in -eq $null) -or ($out -eq $null))
{
    $scriptName =$MyInvocation.InvocationName
    $scriptName = [System.IO.Path]::GetFileName($scriptName)

    echo ""
    echo "usage: ${scriptName} -ca (ca folder) -in (csr file/dir) -certype (client|server|codesign) -out (cert output file/dir) -days (valid days default 365)"
    echo ""
    return
}

if( $days -eq $null)
{
    $days = 365
} 

if( $days -lt 1)
{
    $days = 1
}

if( $certType -eq $null)
{
    $certType = "all"
} 

#see if there is ca folder
if( -not (Test-Path -Path ${ca} -PathType Container))
{
    echo ""
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo " CA folder not found!"
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo ""
    return
}

#see if there is cakey.pem in "private" folder in CA folder
if( -not (Test-Path -Path "${ca}\private\cakey.pem" -PathType Leaf))
{
    echo ""
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo " CA private key not found!"
    echo " CA folder must have cakey.pem in [private] folder"
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo ""
    return
}

#see if there is cacert.pem in CA folder
if( -not (Test-Path -Path "${ca}\cacert.pem" -PathType Leaf))
{
    echo ""
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo " CA certificate not found!"
    echo " There must be cacert.pem in CA folder"
    echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    echo ""
    return
}

$csr_files=""

$dirLevel =""
$dirLevelCert=""

$outIsDir=$false

#save the orignal -in/-out argguments
$orgIn = $in
$orgOut = $out

# Convert to absolute Paths
$in = Resolve-FullPath($in);
$out = Resolve-FullPath($out);
$ca = Resolve-FullPath($ca);

# save current directory
$curDir = Get-Location

# go to script directory
Set-Location $PSSCriptRoot

# On exit, the current directory will be where it was on program start
try
{
    # -in option check -  file or directory?
    if( -not (Test-Path -Path $in -PathType Leaf))
    {
        if( -not (Test-Path -Path $in -PathType Container))
        {
            echo "-in does not exist"
            return  
        }
        else
        {
            #special case : CSR folder in the parent folder
            if( $orgIn -eq "." )
            {
                #relative to script directory
	            $dirLevel = ".\"

	            $loop=0

	            # try to find CSR folder in the script directory or parent folders
	            do
	            {
	                if( (Test-Path -Path $dirLevel"CSR" -PathType Container) -and -not(Test-Path -Path $dirLevel"private" -PathType Container) )
	                {
	                    $csr_files = Get-ChildItem $dirLevel"CSR\*.csr" | Select-Object -ExpandProperty Name
	                    break;
	                }

	                #go up directory tree
	                $dirLevel = "..\" + $dirLevel
	                $loop++
	            }
	            while( $loop -lt 3)

	            if( $loop -eq 3 )
	            {
	                echo "CSR folder not found"
	                return
	            }

                $in = Resolve-FullPath($dirLevel + "CSR")
	        }
	        else
	        {
                $csr_files = Get-ChildItem $in"\*.csr" | Select-Object -ExpandProperty Name
	        }

            if( $csr_files -eq $null )
            {
                echo ""
                echo "xxx No csr file found in ${in} xxx"
                return
            }
        }
    }
    else
    {
        #-in is a file
        $csr_files = [System.IO.Path]::GetFileName($in)
        $in = Split-Path $in -Parent
    }

    # -out check - file or directory
    if( Test-Path -Path $out -PathType Container)
    {
        #relative to script directory
        $outIsDir=$true

        #special case : CRT folder in the parent folder
        if( $orgOut -eq "." )
        {
            $dirLevelCert = ".\"

            $loop=0

            # try to find CRT folder in parent folders
            do
            {
                if( (Test-Path -Path $dirLevelCert"CRT" -PathType Container) -and -not(Test-Path -Path $dirLevelCert"private" -PathType Container) )
                {
                    break;
                }

                #go up directory tree
                $dirLevelCert = "..\" + $dirLevelCert
                $loop++
            }
            while( $loop -lt 3)

            if( $loop -eq 3 )
            {
                echo "CRT folder not found"
                return
            }

            $out = Resolve-FullPath($dirLevelCert + "CRT")
        }
    }

    #prompt for signing CA's private key password
    echo ""
    $pwd_string = Read-Host "Enter Signing CA's private key Password" -AsSecureString
    echo ""

    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd_string)
    $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

    Set-Location $ca


    # iterate files in the directory
    foreach ($csr_file in $csr_files)
    {
        $csr_path = $in + "\" +  $csr_file 

        if( $outIsDir)
        {
            $crt_path = $out + "\" + [System.IO.Path]::GetFileName($csr_path) + ".crt"
        }
        else
        {
            $crt_path = $out
        }

        signCSR $ca $certType $csr_path $crt_path $days
    }

}
finally
{
    Set-Location $curDir
    echo ""
}
```

出力された　mycodesign.crtがコード署名用証明書、myCAフォルダ内の cacert.pem がルートCAのCA証明書になります。