- 投稿日:2019-03-02T22:45:54+09:00
C#プログラミングメモ
とある理由でWebアプリケーション版文字変換システム(Base64/URLのエンコード・デコード)が環境依存により利用できなくなると判明したため,スタンドアローンで利用できる,Windowsアプリケーション版文字変換システムを作成した.その際利用したメソッド等のメモを残しておく.
1.開発環境
Visual Studio2015のCommunityバージョンを用いてWindowsアプリケーションを作成した.
Microsoft Visual Studio Community 2015 Version 14.0.24720.00 Update 1 Microsoft .NET Framework Version 4.6.01055 インストールされているバージョン:Community Visual Basic 2015 00322-20000-00000-AA686 Microsoft Visual Basic 2015 Visual C# 2015 00322-20000-00000-AA686 Microsoft Visual C# 2015 Visual C++ 2015 00322-20000-00000-AA686 Microsoft Visual C++ 2015 Application Insights Tools for Visual Studio のパッケージ 4.2.60128.3 Application Insights Tools for Visual Studio ASP.NET and Web Tools 2015.1 (Beta8) 14.1.11106.0 ASP.NET and Web Tools 2015.1 (Beta8) ASP.NET Web Frameworks and Tools 2012.2 4.1.41102.0 For additional information, visit http://go.microsoft.com/fwlink/?LinkID=309563 ASP.NET Web Frameworks and Tools 2013 5.2.30624.0 For additional information, visit http://www.asp.net/ Common Azure Tools 1.5 Azure Mobile Services および Microsoft Azure Tools で使用する共通サービスを提供します。 Microsoft Azure Mobile Services Tools 1.4 Microsoft Azure Mobile Services Tools NuGet パッケージ マネージャー 3.3.0 Visual Studio 内の NuGet パッケージ マネージャー。NuGet の詳細については、http://docs.nuget.org/ にアクセスしてください。 PreEmptive Analytics Visualizer 1.2 Microsoft Visual Studio extension to visualize aggregated summaries from the PreEmptive Analytics product. SQL Server Data Tools 14.0.50616.0 Microsoft SQL Server Data Tools TypeScript 1.7.6.0 TypeScript for Microsoft Visual Studio2.キーポイント
システム全体のつくりとしては,Base64/URLエンコード・デコードおよび文字参照のエンコード・デコードに対応するアプリケーションを作成した.
2.1.Base64
Base64エンコード・デコードにおいて取り扱う文字コードは,「UTF-8」「Shift-JIS」「EUC-JP」「JIS(iso-2022-jp)」である.
ユーザに文字コードおよびエンコード・デコードを指定してもらい,テキストボックスに入力された文字を変換し,別のテキストボックスに
結果を表示する.技術的に見てみると,文字列を指定された文字コードに従ってバイト型で変数に格納し,バイト型で格納されている文字列をbase64エンコード・デコードしている.
ソースコード// テキストボックス(base64_textbox_before)に入力された文字列を取得する string input_str = base64_textbox_before.Text; // 指定された文字コードでインスタンス化する Encoding enc = Encoding.GetEncoding(base64_radio_button); // 指定された文字コードに従い,バイト型で取り扱う byte[] convert_str = enc.GetBytes(input_str); // Base64エンコードし,文字列として結果を保存する(注:引数はコードページ値もしくはWebNameプロパティ値であること) string after_str = Convert.ToBase64String(convert_str); // デコードする場合,以下の方法でデコードする byte[] convert_str = Convert.FromBase64String(input_str); Encoding enc = Encoding.GetEncoding(base64_radio_button); string after_str = enc.GetString(convert_str); // 変換結果をもう一つのテキストボックス(base64_textbox_after)に表示する base64_textbox_after.Text = after_str;2.2.URLエンコード
URLエンコード・デコードにおいて取り扱う文字コードは,「UTF-8」「Shift-JIS」「EUC-JP」である.
Base64機能と同じで,ユーザに文字コードおよびエンコード・デコードを指定してもらい,結果を表示させる.
技術的に見てみると,string型で取り扱うためBase64の時より1ステップ少なくなる.ソースコード// テキストボックスに入力された文字列を取得 string input_str = url_textbox_before.Text; // 指定された文字コードを取得 System.Text.Encoding charcode = System.Text.Encoding.GetEncoding(url_radio_button); // 文字列を指定された文字コードに従い,URLエンコードする string after_str = System.Web.HttpUtility.UrlEncode(input_str,charcode); // デコードの場合,以下のメソッドを利用する string after_str = System.Web.HttpUtility.UrlDecode(tmp_str,charcode); // 変換結果をもう一つのテキストボックスに表示する url_textbox_after.Text = after_str;2.3.文字参照
課題が残る機能ではあるが,デコードの場合,問題なく数値文字参照および文字実体参照の対応する文字列が表示されるが,エンコードの場合,文字実体参照の結果しか表示されない.原因は,使用しているメソッドが文字実体参照しか取り扱っていないため.そのうち機能追加する予定.
ソースコード// 文字実体参照にエンコードする string after_str = System.Web.HttpUtility.HtmlEncode(input_str); // 数値文字参照および文字実体参照をデコードする string after_str = System.Web.HttpUtility.HtmlDecode(input_str);3.Appendix
3.1.「Ctrl+A」の有効化
テキストボックス内部においてデフォルトでは「Ctrl+A」による全選択が無効化されている.
以下を追加して,有効化する.ソースコード// 以下を対応するForm.Designer.csに追記する this.[textbox_name].KeyDown += new System.Windows.Forms.KeyEventHandler(this.[textbox_name]_KeyDown); // 以下を対応するForm.csに追記する private void [textbox_name]_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.A) { [textbox_name].SelectAll(); } }3.2.comboBoxの初期値設定
フォームデザインにおけるcomboBoxは,プロパティから初期値を設定する方法がよくわからない.
そこで,強制的に初期値を設定する.なお,文字列コレクションの上から順にindexが0,1,2,・・・というように扱われている.ソースコード// 以下を対応するForm.Designer.csに追記する // 今回はindex=0(文字列コレクションの一番上の文字列)を初期値とする this.comboBox1.SelectedIndex = 0;
- 投稿日:2019-03-02T17:37:18+09:00
UnityCameraにAddComponentするだけで3DViwerを作る
Unityに使い慣れる為に、3DViwerを作った時に、一つのファイルにまとまって、CameraにAddComponentするだけで出来た為、そのメモ。
3Dゲームをやっていると、図鑑とかキャラの詳細で、キャラをぐりぐり360°見れるようにするのは、良くある話だと思います。
方法も既に、確立していて割と誰が作成してもほとんど同じ方法で実現されると思っていますが、どうしても簡単な数学が必要で、そこを理解していないとなかなか実現が難しいという話です。方法
以下の式がカメラの移動を表現しており、半径(r)が拡大・縮小の役割を行い、sin/cosの計算が回転の役割を行い、aでカメラと視点を移動させる事で、平行移動を行います。
- x = r * sin θ * cos φ + a
- y = r * sin θ * sin φ + a
- z = r * cos θ + a
拡大
マウスホイールの変化量を、rに増減させます。
スマホアプリだと、ピンチイン/アウトになる為、その変化量を増減させる事になります。
また、サンプルコードだと、最小値を入れていない為、視点に減り込みますので、対応する必要があるかと思います。回転
極座標系の球面座標の考え方を用いる。式をそのまま使えます。
縦の移動量をθの角度の変化量に適用し、横の移動量をφの変化量に適用します。
スマホアプリだと、指一本のフリックやスワイプ操作で行うのが一般的でしょう。
平行移動
特定の操作(サンプルコードの場合だと、右ボタン操作)で、縦横の変化量をaへ増減させます。コメントも入れていますが、transform.rightとtransform.upは単位ベクトルが入っている為、移動方向に使用しています。
スマホアプリだと、指二本のスワイプ操作が一般的なのでしょうか。コード
実際に書いてみたコードです。検証用に書いた為、品質は保証ありません。
Inputで入力を取っている為、マウスだけで簡単に試す事ができます。using UnityEngine; using System.Collections; namespace App { public class PolarCoordinatesCameraController : MonoBehaviour { enum MouseDirection { None, Front, Back, } enum MouseClickType { None, Left, Right, } [SerializeField] float wheelLength = 2f; [SerializeField] float wheelSpeed = 0.1f; [SerializeField] float mouseLeftSpeed = 0.1f; [SerializeField] float mouseRightSpeed = 0.001f; Vector3 cameraLookAt = new Vector3(0f, 0f, 0f); Vector3 cameraPostion = new Vector3(0f, 0f, 0f); Vector2 mouseTouchStartPos; MouseClickType mouseClickType = MouseClickType.None; bool isMouseClick = false; // Fixed:初期Ⅰ。調整してこの角度 float pie = -118f; float fhi = 310f; float radius = 3f; void Update () { var wheel = Input.GetAxis("Mouse ScrollWheel"); if (wheel == 0f) { var moveX = 0f; var moveY = 0f; var moveZ = 0f; if (Input.GetMouseButtonDown(0)) { mouseTouchStartPos = Input.mousePosition; mouseClickType = MouseClickType.Left; isMouseClick = true; } else if (Input.GetMouseButtonDown(1)) { mouseTouchStartPos = Input.mousePosition; mouseClickType = MouseClickType.Right; isMouseClick = true; } else if (Input.GetMouseButton(0)) { moveX = (Input.mousePosition.x - mouseTouchStartPos.x) * mouseLeftSpeed; moveY = (Input.mousePosition.y - mouseTouchStartPos.y) * mouseLeftSpeed; mouseTouchStartPos = Input.mousePosition; } else if (Input.GetMouseButton(1)) { var moveVectorX = Input.mousePosition.x - mouseTouchStartPos.x; var moveVectorY = Input.mousePosition.y - mouseTouchStartPos.y; // memo : transform.right / transform.up は正規化済 var moveRight = transform.right * moveVectorX * mouseRightSpeed; var moveUp = transform.up * moveVectorY * mouseRightSpeed; moveX = moveRight.x + moveUp.x; moveY = moveRight.y + moveUp.y; moveZ = moveRight.z + moveUp.z; mouseTouchStartPos = Input.mousePosition; } else if (Input.GetMouseButtonUp(0) || Input.GetMouseButtonUp(1)) { mouseTouchStartPos = Vector2.zero; } if (isMouseClick && mouseClickType == MouseClickType.Left) { pie -= moveY; fhi -= moveX; } else if (isMouseClick && mouseClickType == MouseClickType.Right) { cameraLookAt.x -= moveX; cameraLookAt.y -= moveY; cameraLookAt.z -= moveZ; cameraPostion.x -= moveX; cameraPostion.y -= moveY; cameraPostion.z -= moveZ; } } else { var direction = MouseDirection.None; if (Input.GetAxis("Mouse ScrollWheel") < 0) { if (wheelLength < radius) { direction = MouseDirection.Front; } } else { direction = MouseDirection.Back; } switch(direction) { case MouseDirection.Front: radius -= wheelSpeed; break; case MouseDirection.Back: radius += wheelSpeed; break; } } var angelPie = pie * 0.01f; var angelFhi = fhi * 0.01f; var pos = new Vector3( radius * Mathf.Sin(angelPie) * Mathf.Sin(angelFhi) + cameraPostion.x, radius * Mathf.Cos(angelPie) + cameraPostion.y, radius * Mathf.Sin(angelPie) * Mathf.Cos(angelFhi) + cameraPostion.z ); transform.position = pos; transform.LookAt(cameraLookAt); } } }Inspectorの画像から、CameraにこのComponentの記事を追加しているだけという事が分かると思います。
最後に
3DViewerも2DViewerも作ったのですが、基本用いる式は同じですが、インターフェース的に、全く同じやり方は出来ない為、少し実現方法を変える必要があります。例えば、回転をカメラに適用してしまうと背景もクルクル回る為、操作している人の気分を害してしまいそうだとか。
時間が取れたら、2DViewer記事も上げていきたいと思います。
- 投稿日:2019-03-02T16:03:17+09:00
FIDO2セキュリティキーで電子署名をする試み
はじめに
世界のパスワード問題を解決し、パスワードのない世界 を目指して策定されたFIDO規格。
FIDO2のWebAuthn-JavaScriptを各社のウェブブラウザが実装したことでWeb認証はもはやFIDOがスタンダードです。
FIDO2のセキュリティキーは 堅牢 かつ パスワードがいらないシンプルな操作 でログインすることができます。一方で デジタルガバメント を目指して策定されたGPKI、JPKI。
マイナンバーカードは券面に個人情報印刷しまくりで、人に見せてはいけない、という意味不明のアナウンスで絶賛普及推進中。
・・・何やっても全く普及せず、ひたすら残念な状況のマイナンバーカードですが、ICカードの中の仕組みはちゃんとしていて、認証だけでなく電子署名
の機能も備えています。つまりログインだけでなく、電子文書へのデジタル署名(電子的な実印)を作成することができます。カード中の秘密鍵はセキュアに保護されており、外に出すことのできる公開鍵はスキャンされても問題ない、マイナンバーカードの機能は携帯する個人認証デバイスとしてはとってもいいものなのです。こうやって見ると
デバイス 認証 電子署名 状況 FIDO2セキュリティーキー 〇 イケイケ マイナンバーカード 〇 〇 残念 FIDO2セキュリティキーで電子署名ができれば最強では。
というわけで、この投稿ではFIDO2セキュリティキーで電子署名をする、というツッコミどころ満載な試みにトライしてみました。
環境
要はFIDO2セキュリティキーを使った電子署名のプログラムを作成する、ということです。
セキュリティキーは指紋スキャナの付いたBioPassを使いますが、BioPassでないといけない、ということはありません。YubikeyなどFIDO2のAuthenticatorであればOKです。OSはWindows、言語はC#、暗号化ライブラリはBouncy Castle、セキュリティキーの制御はWebAuthnModokiDesktopを使います。
- FIDO2セキュリティキー BioPass
- Windows10 1809
- .Net Framework 4.6.1
- Visual Studio 2017 C#
- Bouncy Castle 1.8.5
- WebAuthnModokiDesktop
- ソース GitHub
ユースケース
おおざっぱに以下の構成です。
Phase
- Administrator-Register
- User-Register
- Signature
- VeriyActor
- システム管理者
- ユーザー
- マネージャ
株式会社GEBOは三文判による紙の社内文書の運用から電子署名運用に切り替えました。という体で。
- システム管理者=社内システム担当:下暮田
- ユーザー=新入社員:ゲボ子
- マネージャ=ゲボ子の上司:毛保川◆Administrator-Register
情報処理室の社内システム担当:下暮田さんは本日入社する社員のゲボ子さんに渡すセキュリティキーを作成します。
セキュリティキー(BioPass)は新品のものを使いますが、念のために初期化しておきましょう。
初期化はBio Pass FIDO2 Managerというメーカー提供のツールでリセットしておきます。(Microsoft StoreからGETできます)
登録はGeboSigRegisterというアプリで行います。
登録アプリ処理
- RSA1024キーペア生成
- 秘密鍵をPEM形式でGET
- PEM形式の秘密鍵をDER形式に変換する
- DER形式の秘密鍵をAES256で暗号化する
- セキュリティキーにPINを設定する
- セキュリティキーに秘密鍵を書き込む
- 証明書を作成する
RSA1024キーペア生成
RSA-1024bitキーペアを生成します。
Bouncy CastleのAPIを使います。
※Bouncy Castleはusingの追加がたくさん必要なので、Alt+Enterでどんどん追加していきましょう。RSA-1024bitキーペアを生成private AsymmetricCipherKeyPair createKeyPair() { var randGen = new CryptoApiRandomGenerator(); var rand = new SecureRandom(randGen); var param = new KeyGenerationParameters(rand, 1024); var keyGen = new RsaKeyPairGenerator(); keyGen.Init(param); var keyPair = keyGen.GenerateKeyPair(); return (keyPair); }秘密鍵をPEM形式でGET
Bouncy CastleのPemWriterを使えば簡単です。
秘密鍵をPEM形式でGETprivate string getPrivatekyPEM(AsymmetricCipherKeyPair keyPair) { var mem = new MemoryStream(); using (var writer = new StreamWriter(mem, Encoding.ASCII)) { var pemWriter = new PemWriter(writer); pemWriter.WriteObject(keyPair.Private); pemWriter.Writer.Flush(); } var pem = Encoding.UTF8.GetString(mem.ToArray()); return (pem); }PEM形式の秘密鍵をDER形式に変換する
少しでもサイズを小さくしたいのでDERにします。これはBouncy CastleのAPIが見つからず、定番の方法(?)でやります。
PEMからDERにするpublic static byte[] ConvertPEMtoDER(string pem) { var pems = pem.Trim('\n').Split('\n').ToList(); // ヘッダとフッダは飛ばす pems.RemoveAt(0); pems.RemoveAt(pems.Count - 1); // つなげる var base64 = String.Join("", pems); // もどす return (Convert.FromBase64String(base64)); }DER形式の秘密鍵をAES256で暗号化する
Bouncy Castleを使います。
こちらの素晴らしいサンプルをコピペさせていただきました
https://kagasu.hatenablog.com/entry/2017/01/04/213533keyとivは普通はオープンにしてはいけませんよ。
AES256// inDataがDER形式の秘密鍵です public static byte[] Encrypt(byte[] inData) { key = Encoding.UTF8.GetBytes("ygmh8zudlw5u0a9w4vc29whc4b8wuech"); iv = Encoding.UTF8.GetBytes( "o10wi1q3x2f98cobfkyisnwy9s9wxop7"); // Rijndael // Mode = CBC // BlockSize = 256bit // PaddingMode = Zero var cbcBlockCipher = new CbcBlockCipher(new RijndaelEngine(256)); cipher = new PaddedBufferedBlockCipher(cbcBlockCipher, new ZeroBytePadding()); parametersWithIV = new ParametersWithIV(new KeyParameter(key), iv); cipher.Init(true, parametersWithIV); var bytes = new byte[cipher.GetOutputSize(inData.Length)]; var length = cipher.ProcessBytes(inData, bytes, 0); cipher.DoFinal(bytes, length); return bytes; }セキュリティキーにPINを設定する
初期PINをセットします。
USBにセキュリティキーを挿してから、WebAuthnModokiDesktopのAPIで一発です。PIN設定// PINは1234 private async Task<bool> setNewPIN() { var status = await WebAuthnModokiDesktop.credentials.setpin(gebo.CTAP2.DevParam.getDefaultParams(), "1234"); if( status.isSuccess == false) { // Error return false; } return true; }セキュリティキーに秘密鍵を書き込む
さて、登録のヤマ場です。
FIDO2セキュリティキーにどうやって秘密鍵を書き込むのか、ですが、ResidentKeyの機能を利用します。
ResidentKeyはRPのユーザー情報を書き込むための機能で、秘密鍵などというものを書き込むための機能ではありません。 なのですが、やってみましょう。※注意※
この制限はFIDOとかCTAPの仕様によるものではないと思われますので、モノによってこの通りでないと思われます。実際に試したところ、1回で書き込める情報は以下の通りでした。
- UserID領域に64byte(byte型)
- UserName領域に64文字(string型)
- DisplayName領域に64文字(string型)これ以上のデータを書き込もうするとエラーになります。
string型は1byteを2文字のHEXにすればいいか、ということで、64byte+32byte(64文字)+32byte(64文字)の合計128byteを1回で書き込むことができます。
でありますため、何回かに分けて書き込むことにします。1回に書き込むデータを1レコードとして、レコードの構造は以下の通り。
Area DataType Size 内容 UserID領域 レコードNo 1byte 0から始まるレコード番号 UserID領域 予備 1byte 0xFF固定 UserID領域 データ 62byte 書き込むデータ UserName領域 データ 64文字 32byteのデータをHEX64文字にする DisplayName領域 データ 64文字 32byteのデータをHEX64文字にする 先ほど作成した秘密鍵(AES256で暗号化したもの)は640byteでした。
これだと6レコードになります。
6回に分けて書き込みます。書き込み自体はWebAuthnModokiDesktopのAPIを使えば簡単です。
- 引数pinはさっき設定した初期PINを指定します。
- 引数recはこんなかんじ
- BioBassだと1回の書き込みにつきUser Presenceのチェックが走るのでキーがピカピカ光ってタッチが必要です、つまり、6回タッチしないいけないです。
- これがNFCタイプ(例えばYubikey5)だとUser Presenceがされないのでいちいちタッチする必要がなく快適です。1レコードの書き込みprivate async Task<string> writeRec(string pin,WriteData rec) { string result = ""; try { result = await Task<string>.Run(async () => { byte[] challenge = System.Text.Encoding.ASCII.GetBytes("this is challenge"); byte[] userid = new byte[] { rec.recno, rec.filler }; userid = userid.ToList().Concat(rec.data1).ToArray(); string username = (rec.data2 == null) ? "" : gebo.CTAP2.Common.BytesToHexString(rec.data2); string userdisplayname = (rec.data3 == null) ? "" : gebo.CTAP2.Common.BytesToHexString(rec.data3); string json = "{" + @"rp : {" + string.Format($"id : 'GeboSig.gebo.com',") + @"}," + @"user : {" + string.Format($"id_bytearray:[{string.Join(",", userid)}],") + string.Format($"name :'{username}',") + string.Format($"displayName :'{userdisplayname}',") + @"}," + @"pubKeyCredParams: [{type: 'public-key',alg: -7}]," + @"attestation: 'direct'," + @"timeout: 60000," + @"authenticatorSelection : {" + string.Format($"requireResidentKey : true,") + @"authenticatorAttachment : 'cross-platform'," + string.Format($"userVerification : 'discouraged'") + @"}," + string.Format($"challenge:[{string.Join(",", challenge)}],") + "}"; var ret = await WebAuthnModokiDesktop.credentials.create(gebo.CTAP2.DevParam.getDefaultParams(), json, pin); if (ret.isSuccess == false) { return ret.msg; } return ("Success"); }); } catch (Exception ex) { result = ex.Message; } finally { } return result; }証明書を作成する
最後に秘密鍵のペアとなる公開鍵の処理です。
公開鍵のままでも別にいいんですけど、せっかくなので(?)X.509形式の証明書にしておきましょう。
Bouncy CastleのAPIなら簡単です。
※自己署名なんで、証明書の意味はないです。
- 証明書のCNには対象者のゲボ子さんの情報を入れておきます。
- 証明書は本日より10年間有効です。
- 証明書に今作成したキーペアの公開鍵を格納し、自分自身の秘密鍵で署名します。
- 証明書はPEM形式で作成します。
- 作成した証明書は保管しておきます。本ユースケースでは社内のRepositoryで保管することにします。private string createCertificate(AsymmetricCipherKeyPair keyPair) { // 証明書の属性 var attr = new Dictionary<DerObjectIdentifier, string>() { { X509Name.CN, geboko@gebo.com }, { X509Name.C, "Japan" }, { X509Name.ST, "None" }, { X509Name.L, "None" } { X509Name.O, "gebo" }, { X509Name.OU, "None" }, }; var ord = new List<DerObjectIdentifier>() { X509Name.CN, X509Name.C, X509Name.ST, X509Name.L, X509Name.O, X509Name.OU, }; // 証明書の生成 var name = new X509Name(ord, attr); var certGen = new X509V3CertificateGenerator(); certGen.SetSerialNumber(BigInteger.One); certGen.SetIssuerDN(name); certGen.SetSubjectDN(name); certGen.SetNotBefore(DateTime.Now); certGen.SetNotAfter(DateTime.Now.AddYears(10)); certGen.SetPublicKey(keyPair.Public); var cert = certGen.Generate(new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id, keyPair.Private)); // 証明書の出力 var mem = new MemoryStream(); using (var writer = new StreamWriter(mem, Encoding.ASCII)) { var pemWriter = new PemWriter(writer); pemWriter.WriteObject(cert); pemWriter.Writer.Flush(); } var pem = Encoding.UTF8.GetString(mem.ToArray()); return (pem); }登録後の運用
下暮田さんの作業です。
- 生成されたゲボ子さん用の証明書(geboko.crt)は社内のRepositoryにしまいます。
- 初期化が済んだセキュリティキーをゲボ子さんに渡します。
◆User-Register
システム管理者:下暮田さんから自分用のセキュリティキーを受け取ったゲボ子さんは自分用の初期設定をします。
- PINの変更
- 指紋登録PINの変更はGeboSigChangePINというアプリで行います。
PIN変更アプリ処理
WebAuthnMODOKIDesktopのAPIで一発です。
// newpin=新しいPIN、currentpin=現在のPIN var devParam = gebo.CTAP2.DevParam.getDefaultParams(); var ret = await WebAuthnModokiDesktop.credentials.changepin(devParam, newpin, currentpin); if (ret.isSuccess == true) { // OK } else { // NG }指紋登録
指紋登録はBio Pass FIDO2 Managerというメーカー提供のツールで行います。(Microsoft StoreからGETできます)
これでこのセキュリティキーのPINを知っているのはゲボ子さんだけす。指紋もゲボ子さんのものを登録しているので、セキュリティキーを使えるのはゲボ子さんだけ、ということになります。
◆Signature
ここからが通常運用です。
ゲボ子さんは社内用の報告書を作成しました。上司に電子署名付きのファイル(報告書)を送ります。署名はGeboSigSignatureというアプリで行います。
署名アプリ処理
- 暗号化された秘密鍵を取り出す
- 復号する
- 復号データからパディングデータを除去する
- DERからPEMに変換する
- 電子署名を作成する
- 署名の付いた文書を作成する
暗号化された秘密鍵を取り出す
セキュリティキーの中から(暗号化された)秘密鍵を取り出します。
セキュリティキーのアクセスはUV-指紋認証です。つまり ゲボ子さん本人 しかアクセスできません。
非常用でPINでのアクセスもできるようにしとかないといけないですが、PINも本人しか知らないはずです。秘密鍵はセキュリティキーの中に複数レコードに分割して書き込みました。
セキュリティキーからレコード情報を取りだすのはWebAuthnModokiDesktopのAPIで一発です。
取り出したデータのUserID、UserName、DisplayNameを格納時と逆の方法でつなぎ合わせてbyte配列にします。
- 戻り値ReadDataはこんなかんじ
- credentialid は使わないので空です。
- 指紋認証なのでuserVerification : 'preferred'にしますデータ取り出しprivate async Task<ReadData> readRecs() { ReadData result; try { result = await Task<ReadData>.Run(async () => { var readData = new ReadData(); byte[] challenge = System.Text.Encoding.ASCII.GetBytes("this is challenge"); var credentialid = new byte[0]; string json = "{" + string.Format($"timeout : 60000,") + string.Format($"challenge:[{string.Join(",", challenge)}],") + string.Format($"rpId : 'GeboSig.gebo.com',") + @"allowCredentials : [{" + string.Format($"id : [{string.Join(",", credentialid)}],") + string.Format($"type : 'public-key',") + @"}]," + string.Format($"requireUserPresence : 'false',") + string.Format($"userVerification : 'preferred',") + "}"; var ret = await WebAuthnModokiDesktop.credentials.get(gebo.CTAP2.DevParam.getDefaultParams(), json, ""); if (ret.isSuccess == false) { readData.isSuccess = false; readData.msg = ret.msg; return readData; } // dataList var dataList = new List<WriteData>(); foreach (var assertion in ret.assertions) { dataList.Add(new WriteData(assertion.User_Id, assertion.User_Name, assertion.User_DisplayName)); } dataList = dataList.OrderBy(x => x.recno).ToList(); // data readData.data = new byte[0]; foreach (var data in dataList) { var tmp = data.data1.ToList().Concat(data.data2).Concat(data.data3).ToList(); readData.data = readData.data.ToList().Concat(tmp).ToArray(); } readData.isSuccess = true; readData.msg = "Success"; return readData; }); } finally { } return result; }復号する
さて、取り出した秘密鍵はAES256で暗号化されているので復号します。
復号するときのkeyとivは暗号化するときと同じものを指定しましょう。keyとivは普通はオープンにしてはいけませんよ。
復号する// inDataが暗号化されているデータです public static byte[] Decrypt(byte[] inData) { // AES-256 key = Encoding.UTF8.GetBytes("ygmh8zudlw5u0a9w4vc29whc4b8wuech"); iv = Encoding.UTF8.GetBytes( "o10wi1q3x2f98cobfkyisnwy9s9wxop7"); // Rijndael // Mode = CBC // BlockSize = 256bit // PaddingMode = Zero var cbcBlockCipher = new CbcBlockCipher(new RijndaelEngine(256)); cipher = new PaddedBufferedBlockCipher(cbcBlockCipher, new ZeroBytePadding()); parametersWithIV = new ParametersWithIV(new KeyParameter(key), iv); cipher.Init(false, parametersWithIV); var bytes = new byte[cipher.GetOutputSize(inData.Length)]; var length = cipher.ProcessBytes(inData, bytes, 0); var ret = cipher.DoFinal(bytes, length); return bytes; }復号データからパディングデータを除去する
ここが今一つわからなかったのですが、AESの復号ではパティングデータが付いたままでモドされるようです。PaddingMode = Zeroなので後ろに0x00が何個かひっついてきます。
これがあると非常に具合が悪いので除去します。
幸いなことにDERは先頭SEQにデータレングスがあるので、その情報をもとにパディングデータをとっぱらいます。
これでちゃんとしたDERになります。パディングデータを除去するprivate byte[] getPrivateKey(byte[] decData) { if(decData[0] != 0x30){ return (null); } if (decData[1] != 0x82){ return (null); } var datasize = (int)ChangeEndian.Reverse(BitConverter.ToUInt16(decData, 2)); // add header-4byte datasize = datasize + 4; return(decData.ToList().Take(datasize).ToArray()); }DERからPEMに変換する
さてさて、Bouncy Castleで署名するためにPEMにしないといけないです。
めんどくさいなぁ、Bouncy CastleってほんとにDERつかえないのかなぁDERからPEMに変換する// 1.Base64エンコード // 2.64文字ごとに改行コードをいれる // 3.ヘッダとフッタを入れる public static string ConvertPrivateKeyDERtoPEM(byte[] der) { string pemdata = "-----BEGIN RSA PRIVATE KEY-----\n" + ConvertDERtoPEM(der) + "-----END RSA PRIVATE KEY-----\n"; return pemdata; } private static string ConvertDERtoPEM(byte[] der) { var b64cert = Convert.ToBase64String(der); string pemdata = ""; int roopcount = (int)Math.Ceiling(b64cert.Length / 64.0f); for (int intIc = 0; intIc < roopcount; intIc++) { int start = 64 * intIc; if (intIc == roopcount - 1) { pemdata = pemdata + b64cert.Substring(start) + "\n"; } else { pemdata = pemdata + b64cert.Substring(start, 64) + "\n"; } } return pemdata; }電子署名を作成する
ついにこのときが来ました。ファイルの署名を作成します。
Bouncy Castleで署名を作成します。
なにやら色々手順がありますが、思考停止のおまじないということで。
- pemPrivateKeyはPEM形式の秘密鍵。
- targetfilepathは署名対象のファイルのパスとファイル名です。
- ReadAllBytesしているんでファイルを一回全部取り込むっぽいです。巨大なファイルだとヤバいです。電子署名を作成する!private byte[] createSign(string pemPrivateKey, string targetfilepath) { byte[] data = System.IO.File.ReadAllBytes(targetfilepath); // PEMフォーマットの秘密鍵を読み込んで KeyPair オブジェクトを生成 var privateKeyReader = new PemReader(new StringReader(pemPrivateKey)); var keyPair = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject(); RsaKeyParameters key = (RsaKeyParameters)keyPair.Private; ISigner sig = SignerUtilities.GetSigner("SHA1withRSA"); sig.Init(true, key); var bytes = data; sig.BlockUpdate(bytes, 0, bytes.Length); byte[] signature = sig.GenerateSignature(); return signature; }署名の付いた文書を作成する
ターゲットファイルと署名を一つのzipにして固めてデスクトップに吐き出すだけです。
- .netFrameworkのSystem.IO.Compressionで簡単です。
- 署名はsig.sigというファイル名にします。zipに固めるusing System.IO.Compression; private bool createZip(string targetFile, byte[] sig) { var rootDir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); var targetFileTitle = System.IO.Path.GetFileNameWithoutExtension(targetFile); var targetFileName = System.IO.Path.GetFileName(targetFile); var zipFile = $@"{rootDir}\{targetFileTitle}.zip"; // zipに固める using (var z = ZipFile.Open(zipFile, ZipArchiveMode.Update)) { z.CreateEntryFromFile(targetFile, targetFileName, CompressionLevel.Optimal); ZipArchiveEntry item = z.CreateEntry("sig.sig",CompressionLevel.Optimal); using (Stream stream = item.Open()) { stream.Write(sig, 0, sig.Length); stream.Flush(); } } return true; }署名後の運用
ゲボ子さんの作業です。
- デスクトップに署名付き報告書ファイル(zip)ができるので、それを上司に送り付けます。
◆Veriy
このフェーズではセキュリティキーは使いません
ゲボ子さんの上司:毛保川はゲボ子さんからメールを受け取りました。添付を見ると署名付きzipです。これが本当に本物かどうか検証アプリでチェックします。
とあるデータのVerifyには
- 署名
- 署名した人の公開鍵
が必要になります。検証はGeboSigVerifyというアプリで行います。
検証アプリ処理
- zipからファイルと署名データを取り出す
- 証明書から公開鍵をGETする
- Verifyする
- memo:OpenSSLでVerify
zipからファイルと署名データを取り出す
.netFrameworkのSystem.IO.Compressionで簡単です。
- ディスクにワークファイルを残したくないのでstreamでやってます。zipからデータを取り出すprivate bool getVerifyFileandSig(string zip,out byte[] target, out byte[] sig) { target = null; sig = null; using (System.IO.Compression.ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(zip)) { if( archive.Entries.Count != 2) { return false; } foreach (System.IO.Compression.ZipArchiveEntry entry in archive.Entries) { if (entry.Name == "sig.sig") { sig = new byte[entry.Length]; using (Stream stream = entry.Open()) { var result = stream.Read(sig, 0, (int)entry.Length); } } else { target = new byte[entry.Length]; using (Stream stream = entry.Open()) { var result = stream.Read(target, 0, (int)entry.Length); } } } } return true; }証明書から公開鍵をGETする
ここで、Administrator-Registerフェーズで作成したゲボ子さんの証明書が必要になります。
社内のRepositoryからゲボ子さんの証明書を探してきてその中から公開鍵をGETします。証明書から公開鍵をGETする// certFileは証明書のパスファイル名です private AsymmetricKeyParameter readPublicKeyfromCert(string certFile) { Org.BouncyCastle.X509.X509Certificate readedCert; // 証明書の読み込み using (var reader = new StreamReader(certFile, Encoding.ASCII)) { var pemReader = new PemReader(reader); readedCert = (Org.BouncyCastle.X509.X509Certificate)pemReader.ReadObject(); } var publicKey = readedCert.GetPublicKey(); return (publicKey); }Verifyする
いよいよVerifyです、署名は検証するためにあります。
VerifyはBouncy Castleでやります。
以下のメソッドでtrueとなれば検証OKです。
Verifyによって間違いなくゲボ子さんが書いたものであり、改ざんされていないということが確認できるのです。Bouncy Castleめちゃ楽
Verifyprivate bool verify(byte[] target,byte[] sig,AsymmetricKeyParameter publicKey) { ISigner signer = SignerUtilities.GetSigner("SHA1withRSA"); signer.Init(false, publicKey); signer.BlockUpdate(target, 0, target.Length); var result = signer.VerifySignature(sig); return result; }memo:OpenSSLでVerify
ちなみにVerifyはこのアプリでなくともできます。
openSSLでやる場合は以下のコマンドでVerifyできます。OpenSSLでVerifyopenssl x509 -in TestUser.crt -pubkey -noout>public-key.pem openssl dgst -sha1 -verify public-key.pem -signature sig.sig とっても大事な文書.pdf検証後の運用
毛保川の作業です。
- ゲボ子さんから送られてきた報告書を確認し、保管します。
- 電子署名がついているzipのまま保管すればOK
おつかれさまでした
FIDO2セキュリティキーに署名の機能を追加することによって、認証と署名ができるようになります。
認証の機能で社内への入退室をしたり、システムにログインし、署名の機能を使って社内文書の署名したりすることができますね。
生体認証もできるのでいい感じでは。今回はかなり簡易的なやり方でやっているので、このような方法で作られた電子署名がどの程度信頼できるものなのか、ちょっとわかんないんですけど。
しかし署名まわりのいい勉強になりました。おわり。
- 投稿日:2019-03-02T15:39:47+09:00
Dictionary<TKey, TValue>はキーとしてnullを許容しない
Dictionary<TKey, TValue>
の公式ドキュメント公式ドキュメントの注釈より
A key cannot be null, but a value can be, if its type TValue is a reference type.
Dictionary<TKey, TValue>
において、キーとしてnull
は使えません。(参照型の場合。値型はそもそもnullにできない。)を使う際、nullをキーとして呼び出すと、
ArgumentNullException
が投げられます。これは、それらのメソッドの内部でキーの
GetHashCode
メソッドを呼び出す必要があるためです。次のようなコレクション初期化子は内部で、Addメソッドを使っているので、コレクション初期化子でも例外が発生することに注意してください。
// 例外が発生する var dict = new Dictionary<string, int> { { "a", 0 }, { "b", 0 }, { "c", 0 }, { null, 0 }, };
さて、これは実装である「
Dictionary<TKey, TValue>
」の仕様です。のリファレンスには、
Implementations can vary in whether they allow you to specify a key that is null.
とあります。
キーとしてnullを許可するかどうかは、
IDictionary<TKey, TValue>
とIReadOnlyDictionary<TKey, TValue>
を実装するクラスのその実装に任されています。キーとしてnullを許容する
IReadOnlyDictionary<TKey, TValue>
を実装するクラスの例を次に示します。public class NullableKeyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> { private readonly IReadOnlyDictionary<TKey, TValue> source; private readonly bool hasNullKey; private readonly TValue nullValue; public NullableKeyDictionary(IReadOnlyDictionary<TKey, TValue> source) { if (this.source == null) { throw new ArgumentNullException(nameof(source)); } this.source = source; this.hasNullKey = false; this.nullValue = default(TValue); } public NullableKeyDictionary(IReadOnlyDictionary<TKey, TValue> source, TValue nullValue) { if (this.source == null) { throw new ArgumentNullException(nameof(source)); } this.source = source; this.hasNullKey = true; this.nullValue = nullValue; } public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { foreach (var it in this.source) { yield return it; } if (this.hasNullKey) { yield return new KeyValuePair<TKey, TValue>(default(TKey), nullValue); } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int Count => this.hasNullKey ? this.source.Count + 1 : this.source.Count; public bool ContainsKey(TKey key) { if (this.hasNullKey && key == null) { return true; } else { return this.source.ContainsKey(key); } } public bool TryGetValue(TKey key, out TValue value) { if (this.hasNullKey && key == null) { value = nullValue; return true; } else { return this.TryGetValue(key, out value); } } public TValue this[TKey key] => key == null && hasNullKey ? this.nullValue : this.source[key]; public IEnumerable<TKey> Keys { get { foreach (var it in this.source) { yield return it.Key; } if (this.hasNullKey) { yield return default(TKey); } } } public IEnumerable<TValue> Values { get { foreach (var it in this.source) { yield return it.Value; } if (this.hasNullKey) { yield return nullValue; } } } }