aLice de verify - ブロックチェーン symbolで簡単に証明書を検証 -

# はじめに
Defiで騒がれることが多いブロックチェーン界隈ですが、そんな中Symbolは

[関西テレビソフトウェアが災害時にも活用できる認証技術の実証実験に成功](https://www.ktv.jp/info/ktvinfo/2023/20231113/) 

[ブロックチェーン技術を活用した次世代ファイル共有ソリューション Juggle](https://www.juggle.jp/)

[医療大麻の（株）サイアムレイワインターナショナルと岐阜大学、ブロックチェーンを活用した産官学共同研究を本格開始](https://prtimes.jp/main/html/rd/p/000000002.000109629.html)

などなど社会実装例が着々と増えつつあります。

そこで今回アドベントカレンダーネタとして、誰でもできる Verify を取り扱ってみたいなと思い、最近ごにょごにょしている[aLice utils](https://alice-utils.fly.dev/verify) に実装してみました。

事前知識として

https://qiita.com/nem_takanobu/items/9bfe292194b56fdd8d07

このあたりを読んでおくと良いです。

Issuer（学校等や資格発行機関）がHolderに対して証明書を発行し、Verifier(検証者)は、Holder(資格等証明書保有者)が偽物でないことを検証します。

これをSymbolを使って簡単に行います。

aLiceのインストールと初期アカウント設定が必要です。以下記事が参考になります。

https://note.com/nononon_symbol/n/n87e8ef3f89e0

:::note info
興味があるかた向けに今回作成したページの全ソースコードを掲載しておきます。Blazor WebAssenmblyを使用しています。
:::

# 証明書の発行

先程の記事に沿ってまずはaLice utilsでTransferTransactionを作成し証明書を発行してみます。(モザイク作成も可能ですが今回はより簡単な方を選択しました)

これはIssuerの作業です。aLiceにIssuerのアカウントが登録されていることが前提になります。

[aLice utils](https://alice-utils.fly.dev/) の GenerateTransactionでTransferTransactionを選択してください。

| 項目 | 値 |
|:-:|:-:|
| SignerPublicKey  | Issuerの公開鍵  |
| RecipientAddress  | HolerのSymbolAddress  |
| Message  | ※以下のnameの値"symbol_university"の箇所を適切なものに書き換えてください |

```
{
    "type": ["credential"],
    "credentialSubject": {
        "degree": {
            "name": "symbol_university"
        }
    }
}
```

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/df7ebf17-a2d5-7291-0427-c87be8bdca60.png">

そのままSubmitを押してスマホの場合は[to aLice]をタップ、PCの場合はaLiceをインストールしたスマホのカメラアプリ等でQRコードを読み込んでaLiceを起動してください。

そのまま署名してトランザクションが承認されれば証明書の発行が完了です。改竄が不可能に近いブロックチェーンに証明書が刻まれました。

なお、証明書の発行には先程の記事にもあったモザイクやHolderの承認を得たい場合はSourceAddressにIssuer、TargetAddressにHolderを設定したAccountMetadataTransaction等も有用だと思います。

# 検証
さて、これで証明書は発行されましたが、本記事の主である検証を行っていきます。例えばHolderであるBobが「この証明書を見てくれ、俺はこの学校を卒業しているんだ！」と宣言したとしても、そのAddressがBobのものであることを証明できなければ意味がありません。

ここでは、Bobが自分の物だと主張するアカウントの検証を行います。もちろんノーコードで。検証には手数料も不要です。

## 署名データの作成
まずaLice utilsのVerifyページで適当なメッセージを作成します。

https://alice-utils.fly.dev/verify

| 項目 | 値 |
|:-:|:-:|
| SignerPublicKey  | Holderの公開鍵  |
| Message  | なんでも良い、testとかでもOK |

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/a14325fe-6ec0-8779-63f2-ba003bed2e5d.png">

Submitを押して自身がHolderの場合はそのままaLiceで署名します。Verifierの場合はリンクやQRコードをHolderに渡して署名させましょう。その際は署名後の署名データをHolderから受け取ってください。

## 署名データの検証
先程のページのSignボタンを押すとVerifyモードに切り替わります。これはVerifierが行います。

| 項目 | 値 |
|:-:|:-:|
| SignerPublicKey  | Holderの公開鍵  |
| Message  | 先程のMessageと同じ物 |
| Hash  | Holderから受け取った署名データ |

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/302e98b7-b872-4d36-a3c8-c1921c47e467.png">

これらを入力しSubmitをタップします。

`Verify Success!!!`と表示されれば確かにHolderが先程のメッセージに署名したことが確認されました。
もし`Verify Failed...`と表示されれば、そのハッシュは入力された公開鍵と紐づく秘密鍵による署名ではありません。

Successであれば、その公開鍵がHolderが保有するSymbolアカウントのものであることが正しいと証明されました。

# コード
本記事はノーコードで検証を行うというものですが、一応ソースコードはそのまま掲載しておきます。これを解説するのが主ではありませんので、もし不具合や質問などありましたらコメントいただければ可能な限り回答します。

あくまでも動作すること、アドベントカレンダーの記事を書くために用意したことが目的です。

## Requirement
- [Blazor Assembly](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor)
- [blazorstrap](https://blazorstrap.io/V5/V5)
- [CatSdk](https://www.nuget.org/packages/CatSdk/1.0.7) Nugetでインストール

検証の箇所がブラウザではうまく動作しなかったためサーバーサイドで実装しています

## Client
```csharp
// Verify.razor
@page "/verify"
@using BlazorStrap
@using aLice_utils.Client.Services
@using CatSdk.Utils
@using BlazorStrap.Extensions.FluentValidation
@using System.Text
@using System.Text.Json
@inject NavigationManager NavigationManager

<h3>Verify</h3>
<BSRow MarginBottom="Margins.Medium">
    <BSInputGroup>
        <span class="@BS.Input_Group_Text">Mode</span>
        <BSInputCheckbox CheckedValue="@("Verify")" UnCheckedValue="@("Sign")" IsToggle="true" Color="BSColor.Secondary" ValueChanged="@((string value) => OnModeChanged(value))">
            @mode
        </BSInputCheckbox>
    </BSInputGroup>
</BSRow>
@if (mode != "Sign")
{
    <BSForm Model="Model" OnValidSubmit="OnVerify">
        <FluentValidator TValidator="Validator.VerifyValidator"/>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">PublicKey</span>
                <BSInput InputType="InputType.Text" placeholder="PublicKey" @bind-Value="Model.PublicKey"/>
            </BSInputGroup>
            <ValidationMessage For="@(() => Model.PublicKey)" />
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">Message</span>
                <BSInput InputType="InputType.TextArea" placeholder="Message" @bind-Value="Model.Message"/>
            </BSInputGroup>
            <ValidationMessage For="@(() => Model.Message)" />
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">Hash</span>
                <BSInput InputType="InputType.Text" placeholder="Hash" @bind-Value="Model.Hash"/>
            </BSInputGroup>
            <ValidationMessage For="@(() => Model.Hash)" />
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSCol>
                <BSInputGroup MarginBottom="Margins.Medium">
                    <BSButton IsSubmit="true" Color="BSColor.Primary">Submit</BSButton>
                </BSInputGroup>
            </BSCol>
        </BSRow>
    </BSForm>
    @if (isVerified)
    {
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup MarginBottom="Margins.Medium">
                <BSLabel>
                    @verifyResult
                </BSLabel>
            </BSInputGroup>
        </BSRow>   
    }
}
else
{
    <BSForm Model="MessageModel" OnValidSubmit="OnGenerateURL">
        <FluentValidator TValidator="Validator.VerifyMessageValidator"/>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">PublicKey</span>
                <BSInput InputType="InputType.Text" placeholder="空欄の場合はaLiceのメインアカウントが設定されます" @bind-Value="MessageModel.PublicKey"/>
            </BSInputGroup>
            <ValidationMessage For="@(() => MessageModel.PublicKey)" />
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">Message</span>
                <BSInput InputType="InputType.TextArea" placeholder="Message" @bind-Value="MessageModel.Message"/>
            </BSInputGroup>
            <ValidationMessage For="@(() => MessageModel.Message)" />
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSCol>
                <BSInputGroup MarginBottom="Margins.Medium">
                    <BSButton IsSubmit="true" Color="BSColor.Primary">Submit</BSButton>
                </BSInputGroup>
            </BSCol>
        </BSRow>
    </BSForm>
    @if (isCratedAlice)
    {
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup MarginBottom="Margins.Medium">
                <BSButton OnClick="@ToAlice" Color="BSColor.Success">To aLice</BSButton>
            </BSInputGroup>
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSInputGroup>
                <span class="@BS.Input_Group_Text">URL</span>
                <BSInput InputType="InputType.TextArea" placeholder="URL" @bind-Value="alice"/>
                <BSButton Color="BSColor.Secondary" OnClick='()=>CopyToClipboard(alice)'>
                    <i class="fas fa-clipboard"></i>
                </BSButton>
            </BSInputGroup>
        </BSRow>
        <BSRow MarginBottom="Margins.Medium">
            <BSImage Source="@qrSource" Class="img-fluid"></BSImage>
        </BSRow>
    }
}

@code {
    private string mode = "Sign";
    
    public VerifyModel Model { get; } = new ();
    public VerifyMessageModel MessageModel { get; } = new ();

    private string? alice { get; set; }
    private bool isCratedAlice { get; set; }
    private bool isVerified { get; set; }
    
    private string? verifyResult { get; set; }
    private string? qrSource { get; set; }
    
    private void OnModeChanged(string newValue)
    {
        mode = newValue;
        StateHasChanged();
    }
    
    private async void OnGenerateURL()
    {
        alice = $"alice://sign?type=request_sign_utf8&data={Converter.Utf8ToHex(MessageModel.Message)}";
        if(MessageModel.PublicKey != "") alice += $"&set_public_key={MessageModel.PublicKey}";
        qrSource = await QRServices.GenerateQR(alice);
        isCratedAlice = true;
        StateHasChanged();
    }
    
    private async void OnVerify()
    {
        try
        {
            var hostUrl = NavigationManager.BaseUri;
            
            using var client = new HttpClient();
            var dic = new Dictionary<string, string>()
            {
                {"message", Model.Message},
                {"hash", Model.Hash},
                {"public_key", Model.PublicKey},
            };
            var content = new StringContent(JsonSerializer.Serialize(dic), Encoding.UTF8, "application/json");
            var response = await client.PostAsync(hostUrl + "CheckVerify", content);
            var res = await response.Content.ReadAsStringAsync();
            verifyResult = res == "true" ? "Verify Success!!!" : "Verify Failed...";
            isVerified = true;
            StateHasChanged();     
        } 
        catch (Exception e)
        {
            verifyResult = e.Message;
            isVerified = true;
            StateHasChanged();
        }
    }
    
    private void ToAlice()
    {
        if (alice != null) NavigationManager.NavigateTo(alice);
    }
    
    private async Task CopyToClipboard(string? text)
    {
        await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
    }
    
    public class VerifyModel
    {
        public string PublicKey { get; set; } = "";
        public string Message { get; set; } = "";
        public string Hash { get; set; } = "";
    }
    
    public class VerifyMessageModel
    {
        public string PublicKey { get; set; } = "";
        public string Message { get; set; } = "";
    }
}
```

## Server
```csharp
// CheckVerifyController.cs
using CatSdk.Symbol;
using CatSdk.Utils;
using Microsoft.AspNetCore.Mvc;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;

namespace aLice_utils.Server.Controllers;

[ApiController]
[Route("[controller]")]
public class CheckVerifyController : ControllerBase
{
    [HttpPost]
    public bool Post([FromBody] Dictionary<string, string> data)
    {
        if (data == null) throw new Exception("data is not correct format");
        if (!data.ContainsKey("message")) throw new Exception("message is nothing");
        if (!data.ContainsKey("hash")) throw new Exception("hash is nothing");
        if (!data.ContainsKey("public_key")) throw new Exception("public_key is nothing");
        var message = data["message"];
        var hash = data["hash"];
        var public_key = data["public_key"];
        
        var signature = new Signature(Converter.HexToBytes(hash));
        var ed25519Signer = new Ed25519Signer();
        ed25519Signer.Init(false, (ICipherParameters) new Ed25519PublicKeyParameters(Converter.HexToBytes(public_key), 0));
        ed25519Signer.BlockUpdate(Converter.Utf8ToBytes(message), 0, Converter.Utf8ToBytes(message).Length);
        return ed25519Signer.VerifySignature(signature.bytes);
    }
}
```