diff --git a/AGENTS.md b/AGENTS.md
index 3206a8af8..414964f7d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,7 +5,7 @@ https://github.com/OpenTouryoProject/OpenTouryo/
と、ソレを使用したサンプル・アプリケーションの開発エージェント用。
-環境のセットアップとビルド手順は [root/Readme.ja.md](root/Readme.ja.md) を参照。
+環境のセットアップ・ビルド・検証の手順は [root/Readme.ja.md](root/Readme.ja.md) を参照。
本書は、その上で**エージェントが守るべきこと**と、**どの文書を見るか**を示す。
**プロジェクト共通の投稿規約は [Contributing.ja.md](Contributing.ja.md) に従う。**
@@ -170,23 +170,17 @@ Copyright ブロックの扱い、`ArgumentException` 系の引数の順、`.bat
### ビルド・テスト・リリースの検証は、専用の文書に従う
-変更を加えたあとの**検証**は、次の文書が一次情報である。
+**検証の手順そのものは、人もエージェントも同じである。**
+入口は [root/Readme.ja.md](root/Readme.ja.md) の「ビルド後の検証」で、
+3 本のスクリプト(ビルド → 単体テスト → 疎通)と、その一次情報
+(`BUILDING.md` / `TESTING.md` / `SMOKETEST.md`、リリース時の作業全体は `RELEASE.md`)は、
+そこから辿れる。**本節は、その上で残るエージェント固有の判断だけを扱う。**
-| 目的 | 読む文書 |
-|---|---|
-| **手順だけを引く(早見)** | [`CHEATSHEET.md`](root/programs/CHEATSHEET.md) |
-| リリース時の作業全体 | [`RELEASE.md`](root/programs/RELEASE.md) |
-| 全ビルドの実行と判定 | [`BUILDING.md`](root/programs/BUILDING.md) |
-| 単体テストの実行と判定 | [`TESTING.md`](root/programs/TESTING.md) |
-| サンプルの疎通確認 | [`SMOKETEST.md`](root/programs/SMOKETEST.md) |
-
-検証は `1_BuildAll.ps1`(ビルド)→ `2_RunAllTests.ps1`(単体テスト)→
-`3_SmokeTest.ps1`(疎通)の 3 本で、いずれも終了コードで合否が分かる。
-**順序は固定**(1 のクリーンとアセンブリ配置が 2・3 の前提)。
-
-**本書にコマンドは書かない。** 引数(`-IgnoreErrors` / `-Only` / `-List` / `-Lang`)と
-合格の目安は [`CHEATSHEET.md`](root/programs/CHEATSHEET.md) 1 節が一次情報である。
-**転記すると、両方を直さないかぎりズレる。**
+**手順だけを引くなら [`CHEATSHEET.md`](root/programs/CHEATSHEET.md) 1 節。**
+引数(`-IgnoreErrors` / `-Only` / `-List` / `-Lang`)・合格の目安・
+実行順(**固定**。1 のクリーンとアセンブリ配置が 2・3 の前提)は、そこが一次情報である。
+
+**本書にコマンドは書かない。転記すると、両方を直さないかぎりズレる。**
`2_RunAllTests.ps1` はワーキング ツリーの `Result*.txt` を書き換える(従来のバッチ運用と同じ)。
**コミットの要否は人が判断する**ため、エージェントは差分を報告するに留める。
diff --git a/root/README.md b/root/README.md
index 2b137494c..09d5babf3 100644
--- a/root/README.md
+++ b/root/README.md
@@ -133,6 +133,9 @@ cd C:\root\programs
| `2_RunAllTests.ps1` | Runs the unit tests and compares the results with the previous ones |
| `3_SmokeTest.ps1` | Starts the sample applications and checks that they work |
+The three can also be run individually. **In that case the order is fixed**,
+because the clean and the assembly deployment done by `1_` are prerequisites of `2_` and `3_`.
+
For the procedures and the criteria, refer to
[`BUILDING.md`](programs/BUILDING.md) / [`TESTING.md`](programs/TESTING.md) /
[`SMOKETEST.md`](programs/SMOKETEST.md) in the same folder.
diff --git a/root/Readme.ja.md b/root/Readme.ja.md
index 68c80d97e..210c809d0 100644
--- a/root/Readme.ja.md
+++ b/root/Readme.ja.md
@@ -131,6 +131,9 @@ cd C:\root\programs
| `2_RunAllTests.ps1` | 単体テストを実行し、結果を前回のものと比較 |
| `3_SmokeTest.ps1` | サンプルアプリケーションを起動して疎通を確認 |
+3 本は個別に実行することもできます。**その場合の実行順は固定**です
+(`1_` のクリーンとアセンブリ配置が `2_` `3_` の前提になっているため)。
+
手順と判定基準は、同じフォルダの
[`BUILDING.md`](programs/BUILDING.md) / [`TESTING.md`](programs/TESTING.md) /
[`SMOKETEST.md`](programs/SMOKETEST.md) を参照してください。
diff --git a/root/programs/CS/Frameworks/ANALYSIS.md b/root/programs/CS/Frameworks/ANALYSIS.md
index 1b982b007..7ff041bda 100644
--- a/root/programs/CS/Frameworks/ANALYSIS.md
+++ b/root/programs/CS/Frameworks/ANALYSIS.md
@@ -193,6 +193,67 @@ private void UOC_SelectCount(TestParameterValue p) // private でよい
---
+### 4.4 **非同期の実体(`~Async` は「別スレッドで同期実行」)**(#402)
+
+**D層に非同期の API は無い。** `BaseDam` は `ExecuteReader` / `ExecuteScalar` /
+`ExecuteNonQuery` の同期版だけを持つ。
+
+上位の `~Async` も、**別スレッドで同期版を走らせているだけ**である。
+
+```csharp
+// CallController.cs / BaseLogic.cs
+return Task<...>.Factory.StartNew(() => {
+ return this.Invoke(...); // 同期版
+});
+```
+
+**これはブロックの移動であって、除去ではない。**
+呼び出し元のスレッドは解放されるが、**プールのスレッドが 1 本、DB 応答の間ずっと塞がる。**
+
+#### **非同期化しない、と判断している**(#402)
+
+業務系の DBMS アプリケーションでは、**非同期のサポートは重要ではない**と判断した。
+
+**1 要求が 1 コネクションを保持する設計だからである。**
+
+```
+UOC_ConnectionOpen で Dam を作り、コネクションとトランザクションを開く
+ → B層の処理がすべて終わるまで保持 → Commit / Rollback
+```
+
+**同時実行数は、スレッド数ではなくコネクション プールの上限で決まる。**
+スレッドを解放できても、プールが空かなければ次には進めない。
+非同期化で増えるのは「**待てる要求の数**」であって、
+「**同時に処理できる要求の数**」ではない。
+
+| 論点 | 既存の機構 |
+|---|---|
+| 同時実行数の制御 | コネクション プール(上限) |
+| 応答しない処理の打ち切り | `FxSqlCommandTimeout`(既定 30 秒) |
+
+**非同期化でしか得られないのは「実行中の中断」だけ**だが、
+業務システムでは利用者が処理を中断する場面が限られ、多くは `CommandTimeout` で足りる。
+
+#### **プロバイダによって非同期の実体が違う**
+
+**足すとしても、一様には効かない。**
+`DbCommand` の `~Async` は、プロバイダが上書きしなければ
+**基底クラスの既定実装=呼び出し元スレッドで同期実行**になる。
+**`await` してもそこで止まる**ため、今より悪くなり得る。
+
+| Dam | ライブラリ | 非同期の実体 |
+|---|---|---|
+| `DamSqlSvr` | `Microsoft.Data.SqlClient` | 真の非同期 |
+| `DamPstGrS` | `Npgsql` | 真の非同期 |
+| `DamMySQL` | `MySql.Data` | **要検証** |
+| `DamManagedOdp` | `Oracle.ManagedDataAccess` | **要検証** |
+| `DamODBC` / `DamOLEDB` | 標準 | **効果なし**(既定実装) |
+
+> **`~Async` という名前は「非同期である」と読まれる。**
+> 効かないプロバイダに足すと、名前だけが非同期になり、実態を隠す。
+
+---
+
## 5. 設定(XML 定義ファイル + appSettings)
### 5.1 XML 定義ファイル(`root/files/resource/Xml/`)
diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/IdToken.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/IdToken.cs
index be8e9092b..0989eb87e 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/IdToken.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/IdToken.cs
@@ -29,6 +29,7 @@
//* ---------- ---------------- -------------------------------------------------
//* 2018/08/22 西野 大介 新規作成
//* 2018/11/28 西野 大介 証明書 & Jwk対応 + jkuチェック対応の追加
+//* 2026/09/10 玄人 幸道 CreateHashが左半分でなく左右のXORを返していたのを修正(#584)
//**********************************************************************************
using System;
@@ -209,6 +210,19 @@ public static bool Verify(string id_token,
///
/// string
/// hash
+ ///
+ /// OIDC の定義(OpenID Connect Core 1.0 3.1.3.6 / 3.3.2.11)どおり、
+ /// ハッシュの**左半分**(先頭 bytes.Length / 2 バイト)を base64url にする。
+ ///
+ /// 2026/09/10 まで ArrayOperator.ShortenByteArray で半分にしていたため、
+ /// 左半分ではなく**左半分と右半分の XOR** になっていた(#584)。
+ /// ShortenByteArray は切り詰めではなく XOR による畳み込みで、
+ /// 鍵長の調整(GetKeyedHash)がその挙動に依存するため、あちらは直さない。
+ ///
+ /// VerifyHash も同じ CreateHash で作り直して比べるため、
+ /// 本フレームワーク同士では誤っていても一致し、気付けなかった。
+ /// **修正前の版で発行された値は、修正後の VerifyHash では検証に失敗する。**
+ ///
public static string CreateHash(string input)
{
// ID Token の JOSE Header にある
@@ -219,8 +233,9 @@ public static string CreateHash(string input)
EnumHashAlgorithm.SHA256);
// 左半分を base64url エンコードした値。
+ // (ShortenByteArray は XOR で畳み込むため使わない。#584)
return CustomEncode.ToBase64UrlString(
- ArrayOperator.ShortenByteArray(bytes, (bytes.Length / 2)));
+ ArrayOperator.CopyArray(bytes, (bytes.Length / 2)));
}
///
diff --git a/root/programs/CS/Frameworks/Readme.txt b/root/programs/CS/Frameworks/Readme.txt
deleted file mode 100644
index 6d09f1fdf..000000000
--- a/root/programs/CS/Frameworks/Readme.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-<フォルダの説明>
-
-● Infrastructure
- Frameworkの一式です。
-
-● Tools
-
- - DPQuery_Tool
- 動的パラメタライズド・クエリ分析ツール
-
- - DaoGen_Tool
- D層自動生成ツール
-
- - DeployZipPackWithHTTP
- Webデプロイ部品
-
- - Encryption
- 暗号化部品
-
-● PortableClassLibrary
- Silverlight, WinStore用部品
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/Program.cs b/root/programs/CS/Frameworks/Tests/TestCode/Program.cs
index f1fea76b9..b0335c30c 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/Program.cs
+++ b/root/programs/CS/Frameworks/Tests/TestCode/Program.cs
@@ -27,6 +27,7 @@
//* 2026/08/18 玄人 幸道 各テストを個別に try で囲むようにした(#564)。
//* 1 つが例外を投げると、以降が実行されなかった。
//* 2026/08/08 玄人 幸道 TestZipV2の呼び出しを追加(#524)
+//* 2026/09/10 玄人 幸道 TestIdTokenの呼び出しを追加(#584)
//**********************************************************************************
using System;
@@ -107,6 +108,10 @@ public static void Main(string[] args)
// DB に接続するテストと前提が異なるため、プロジェクトを分けている。
#endregion
+ #region Framework
+ Program.Run("TestIdToken", TestIdToken.Root);
+ #endregion
+
#region Business
// Touryo.Infrastructure.Business
// GMTMaster
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/README.md b/root/programs/CS/Frameworks/Tests/TestCode/README.md
index 2da272e99..85531e9d7 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/README.md
+++ b/root/programs/CS/Frameworks/Tests/TestCode/README.md
@@ -64,6 +64,12 @@ y_Build_TestCode_Public.bat … ビルド → 実行 → 結果ファイル
| `TestLatebind` | `Public.Reflection` | 遅延バインドによる呼び出し(`NonPublic` を含む) |
| `TestFastReflection` | `Public.FastReflection` | `AccessorCacher` / `InstanceCreator` 等の高速リフレクション(#522) |
+### Framework
+
+| ファイル | 対象 | 見ていること |
+|---|---|---|
+| `TestIdToken` | `Framework.Authentication` | `at_hash` / `c_hash` / `s_hash` の計算(`IdToken.CreateHash` / `VerifyHash`)。**期待値は OIDC 仕様の例示値**(#584) |
+
`TestEmbedded.txt` は `TestResourceLoader` が読む埋め込みリソース。
**`LogicalName` を `TestCode.TestEmbedded.txt` に固定している**
(既定のままだと `TestCodeFx` / `TestCodeCore` で名前が食い違うため)。
@@ -117,6 +123,17 @@ MyDebug.OutputDebugAndConsole(
桁の重みを `256 * j`(掛け算)で求めていたため 3 byte 目から誤っていたが、
8 byte の出力をそのまま期待値にしていたため、いったん見逃した。
+### 「作って、同じ実装で確かめる」は検算にならない
+
+**作る側と確かめる側が同じ実装を使うと、誤っていても一致する。**
+
+`IdToken.VerifyHash` は、`CreateHash` で作り直した値と比べるだけである。
+`CreateHash` は OIDC の定義(ハッシュの左半分)ではなく左右の XOR を返していたが、
+本フレームワーク同士では一致するため、**外部の RP に拒否されるまで気付かれなかった**(#584)。
+
+**期待値には、実装と独立に求めた値を置く。** 仕様の例示値があればそれを使う
+(`TestIdToken` は OpenID Connect Core 1.0 付録 A の値を使っている)。
+
### 境界は必ず跨ぐ
同じ `GetLongFromByte` は、**1〜2 byte だけを見ていると不具合に気付けない。**
@@ -168,7 +185,7 @@ net48 側は `4_Build_CopyAssemblies.bat` が依存 DLL を `Build_net48` へ並
|---|---|
| `PubCmnFunction.GetFileNameNoEx` | 第 2 引数は**パス区切り**。拡張子の区切りだと思って `'.'` を渡すと空文字が返る |
| `ArrayOperator.CopyArray` | **配列を伸ばせない。** コピー長が「コピー先配列の長さ」で固定のため、コピー先を大きくするとコピー元が足りず落ちる。書込開始位置を 0 より後ろにしても落ちる |
-| `ArrayOperator.ShortenByteArray` | 単純な切り詰めではなく、**XOR で畳み込む**(暗号鍵の生成用) |
+| `ArrayOperator.ShortenByteArray` | 単純な切り詰めではなく、**XOR で畳み込む**(暗号鍵の生成用)。**切り詰めに使ってはならない**(`IdToken.CreateHash` がこれで誤った、#584)。切り詰めは `CopyArray` |
| `ArrayOperator.GetLongFromByte` | ビッグ エンディアン。8 byte で最上位ビットが立つと **Int64 の範囲を超えて負になる**(2 の補数として解釈した値) |
| `StringExtractor.GetParameterFromQueryString` | 「値が空」と「名前が無い」を**区別できない**(どちらも `""`) |
| `ObjectInspector` | 再帰の深さは **5 まで**。入口で加算・出口で減算するカウンタで抑えている |
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt b/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
index fb422eb14..f68215b51 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
+++ b/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
@@ -70,11 +70,11 @@ log4net: Setting Collection Property [AddFilter] to object [log4net.Filter.Level
log4net: Created Appender [OPERATION2]
log4net: Adding appender named [OPERATION2] to logger [OPERATION].
log4net: Hierarchy Threshold []
-[2026/08/20 10:54:24,630],[DEBUG],[1],LogIF.DebugLog("ACCESS");
-[2026/08/20 10:54:24,639],[INFO ],[1],LogIF.InfoLog("ACCESS");
-[2026/08/20 10:54:24,640],[WARN ],[1],LogIF.WarnLog("ACCESS");
-[2026/08/20 10:54:24,640],[ERROR],[1],LogIF.ErrorLog("ACCESS");
-[2026/08/20 10:54:24,640],[FATAL],[1],LogIF.FatalLog("ACCESS");
+[2026/09/10 22:27:52,562],[DEBUG],[1],LogIF.DebugLog("ACCESS");
+[2026/09/10 22:27:52,568],[INFO ],[1],LogIF.InfoLog("ACCESS");
+[2026/09/10 22:27:52,569],[WARN ],[1],LogIF.WarnLog("ACCESS");
+[2026/09/10 22:27:52,569],[ERROR],[1],LogIF.ErrorLog("ACCESS");
+[2026/09/10 22:27:52,569],[FATAL],[1],LogIF.FatalLog("ACCESS");
LogIF.Is*Enabled - ACCESS : Debug True / Info True / Warn True / Error True / Fatal True
log4net: configuring repository [log4net-default-repository] using stream
log4net: loading XML configuration
@@ -2963,4 +2963,13 @@ CompiledExpressionCreater
フィールド Field : フィールドの値
存在しないメンバ : 例外 System.ArgumentException
----------------------------------------------------------------------------------------------------
+IdToken.CreateHash
+at_hash(OIDC Core 付録 A) : 77QmUPtjPfzWtF2AnpK9RQ(期待値と一致 : True)
+c_hash(OIDC Core 付録 A) : LDktKdoQak3Pk0cnXxCltA(期待値と一致 : True)
+s_hash(#584) : QCiR2bEHBPw1Pd2r2ZtS_Q(期待値と一致 : True)
+----------------------------------------------------------------------------------------------------
+IdToken.VerifyHash
+左半分(OIDC の定義) : True
+左右の XOR(修正前の値) : False
+----------------------------------------------------------------------------------------------------
log4net: Shutdown called on Hierarchy [log4net-default-repository]
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt b/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
index 47010b38c..2e99c318f 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
+++ b/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
@@ -70,11 +70,11 @@ log4net: Setting Collection Property [AddFilter] to object [log4net.Filter.Level
log4net: Created Appender [OPERATION2]
log4net: Adding appender named [OPERATION2] to logger [OPERATION].
log4net: Hierarchy Threshold []
-[2026/08/20 10:54:29,651],[DEBUG],[2],LogIF.DebugLog("ACCESS");
-[2026/08/20 10:54:29,660],[INFO ],[2],LogIF.InfoLog("ACCESS");
-[2026/08/20 10:54:29,661],[WARN ],[2],LogIF.WarnLog("ACCESS");
-[2026/08/20 10:54:29,662],[ERROR],[2],LogIF.ErrorLog("ACCESS");
-[2026/08/20 10:54:29,662],[FATAL],[2],LogIF.FatalLog("ACCESS");
+[2026/09/10 22:27:55,097],[DEBUG],[2],LogIF.DebugLog("ACCESS");
+[2026/09/10 22:27:55,105],[INFO ],[2],LogIF.InfoLog("ACCESS");
+[2026/09/10 22:27:55,105],[WARN ],[2],LogIF.WarnLog("ACCESS");
+[2026/09/10 22:27:55,106],[ERROR],[2],LogIF.ErrorLog("ACCESS");
+[2026/09/10 22:27:55,106],[FATAL],[2],LogIF.FatalLog("ACCESS");
LogIF.Is*Enabled - ACCESS : Debug True / Info True / Warn True / Error True / Fatal True
log4net: configuring repository [log4net-default-repository] using stream
log4net: loading XML configuration
@@ -2984,4 +2984,13 @@ CompiledExpressionCreater
フィールド Field : フィールドの値
存在しないメンバ : 例外 System.ArgumentException
----------------------------------------------------------------------------------------------------
+IdToken.CreateHash
+at_hash(OIDC Core 付録 A) : 77QmUPtjPfzWtF2AnpK9RQ(期待値と一致 : True)
+c_hash(OIDC Core 付録 A) : LDktKdoQak3Pk0cnXxCltA(期待値と一致 : True)
+s_hash(#584) : QCiR2bEHBPw1Pd2r2ZtS_Q(期待値と一致 : True)
+----------------------------------------------------------------------------------------------------
+IdToken.VerifyHash
+左半分(OIDC の定義) : True
+左右の XOR(修正前の値) : False
+----------------------------------------------------------------------------------------------------
log4net: Shutdown called on Hierarchy [log4net-default-repository]
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/TestIdToken.cs b/root/programs/CS/Frameworks/Tests/TestCode/TestIdToken.cs
new file mode 100644
index 000000000..b218d76b0
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestCode/TestIdToken.cs
@@ -0,0 +1,153 @@
+#region Apache License
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+//**********************************************************************************
+//* クラス名 :TestIdToken
+//* クラス日本語名 :Framework.Authentication.IdTokenのテスト
+//*
+//* 作成者 :玄人 幸道
+//* 更新履歴 :
+//*
+//* 日時 更新者 内容
+//* ---------- ---------------- -------------------------------------------------
+//* 2026/09/10 玄人 幸道 新規作成(#584)
+//**********************************************************************************
+
+using System;
+
+using Touryo.Infrastructure.Framework.Authentication;
+using Touryo.Infrastructure.Public.Diagnostics;
+
+namespace TestCode
+{
+ /// Framework.Authentication.IdTokenのテスト
+ ///
+ /// at_hash / c_hash / s_hash の計算(CreateHash / VerifyHash)(#584)。
+ ///
+ /// <期待値は、実装と独立に求めた値を使う>
+ /// VerifyHash は、同じ CreateHash で作り直した値と比べるだけである。
+ /// **作る側が誤っていても、本フレームワーク同士では一致する。**
+ /// 2026/09/10 まで、CreateHash は左半分ではなく左右の XOR を返していたが、
+ /// これでは気付けなかった。
+ ///
+ /// そのため期待値には、OpenID Connect Core 1.0 付録 A の例示値と、
+ /// #584 で実測に使った値(Python の hashlib で求めたもの)を置く。
+ ///
+ public class TestIdToken
+ {
+ #region public
+
+ /// Root
+ public static void Root()
+ {
+ TestIdToken.TestCreateHash();
+
+ MyDebug.OutputDebugAndConsole("----------------------------------------------------------------------------------------------------");
+
+ TestIdToken.TestVerifyHash();
+ }
+
+ #endregion
+
+ #region private
+
+ /// CreateHash
+ ///
+ /// SHA-256 の**左半分**(先頭 16 バイト)を base64url にした値になること。
+ ///
+ private static void TestCreateHash()
+ {
+ MyDebug.OutputDebugAndConsole("IdToken.CreateHash");
+
+ // OpenID Connect Core 1.0 付録 A の例(access_token → at_hash)
+ TestIdToken.OutputCreateHash("at_hash(OIDC Core 付録 A)",
+ "jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y",
+ "77QmUPtjPfzWtF2AnpK9RQ");
+
+ // OpenID Connect Core 1.0 付録 A の例(code → c_hash)
+ TestIdToken.OutputCreateHash("c_hash(OIDC Core 付録 A)",
+ "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk",
+ "LDktKdoQak3Pk0cnXxCltA");
+
+ // #584 で実測に使った state → s_hash
+ TestIdToken.OutputCreateHash("s_hash(#584)",
+ "state-hybrid",
+ "QCiR2bEHBPw1Pd2r2ZtS_Q");
+ }
+
+ /// VerifyHash
+ ///
+ /// **修正前の版で発行された値(左右の XOR)は、検証に失敗する。**
+ /// 互換性が切れることを、ケースとして残しておく。
+ ///
+ private static void TestVerifyHash()
+ {
+ MyDebug.OutputDebugAndConsole("IdToken.VerifyHash");
+
+ // 左半分(OIDC の定義)
+ TestIdToken.OutputVerifyHash("左半分(OIDC の定義)",
+ "state-hybrid", "QCiR2bEHBPw1Pd2r2ZtS_Q");
+
+ // 左右の XOR(2026/09/10 までの CreateHash が返していた値)
+ TestIdToken.OutputVerifyHash("左右の XOR(修正前の値)",
+ "state-hybrid", "iZPw3gFUI31HD9TANpAVbQ");
+ }
+
+ #endregion
+
+ #region 出力のヘルパ
+
+ /// CreateHash の結果を出力する
+ /// ケース名
+ /// 入力(access_token / code / state)
+ /// 期待値(実装と独立に求めた値)
+ private static void OutputCreateHash(string caseName, string input, string expected)
+ {
+ try
+ {
+ string actual = IdToken.CreateHash(input);
+
+ MyDebug.OutputDebugAndConsole(
+ caseName + " : " + actual + "(期待値と一致 : " + (actual == expected) + ")");
+ }
+ catch (Exception ex)
+ {
+ // メッセージは環境の言語で変わるため、型名だけを出す。
+ MyDebug.OutputDebugAndConsole(caseName + " : 例外 " + ex.GetType().FullName);
+ }
+ }
+
+ /// VerifyHash の結果を出力する
+ /// ケース名
+ /// 入力(access_token / code / state)
+ /// 検証するハッシュ値
+ private static void OutputVerifyHash(string caseName, string input, string hash)
+ {
+ try
+ {
+ MyDebug.OutputDebugAndConsole(
+ caseName + " : " + IdToken.VerifyHash(input, hash));
+ }
+ catch (Exception ex)
+ {
+ // メッセージは環境の言語で変わるため、型名だけを出す。
+ MyDebug.OutputDebugAndConsole(caseName + " : 例外 " + ex.GetType().FullName);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/core100/TestCodeCore.csproj b/root/programs/CS/Frameworks/Tests/TestCode/core100/TestCodeCore.csproj
index 9660f9bc0..19304f633 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/core100/TestCodeCore.csproj
+++ b/root/programs/CS/Frameworks/Tests/TestCode/core100/TestCodeCore.csproj
@@ -20,6 +20,7 @@
+
@@ -58,7 +59,9 @@
+ OpenTouryo.Public / Public.Security が使うパッケージは、ここにも並べる必要がある。
+ BouncyCastle.NetCore は GetHash が使う(IdToken.CreateHash 経由、#584)。-->
+
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/net48/TestCodeFx.csproj b/root/programs/CS/Frameworks/Tests/TestCode/net48/TestCodeFx.csproj
index 0c96a9d4e..093cc5539 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/net48/TestCodeFx.csproj
+++ b/root/programs/CS/Frameworks/Tests/TestCode/net48/TestCodeFx.csproj
@@ -86,6 +86,9 @@
TestGetMessageAndProperty.cs
+
+ TestIdToken.cs
+
TestJISCode.cs
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md
index c0a7d3161..09f306bd0 100644
--- a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md
@@ -4,7 +4,8 @@
Linux コンテナで動かす(#582)。
**構成は [`../MVC_Sample/README.md`](../MVC_Sample/README.md) と同じである。**
-方式(ホストビルド型)・設定の置き方・証明書の扱い・本番へ持って行くときの注意は、
+方式(ホストビルド型)・設定の置き方・証明書の扱い・本番へ持って行くときの注意、
+**起動のたびに publish とイメージ ビルドが走る理由**は、
**あちらが一次情報**なので、ここには書かない。
ここに書くのは**この Resource Server だけの事情**である。
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md
index eefc6147a..3d4292861 100644
--- a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md
@@ -36,6 +36,34 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\0_SetupCert.ps1
ログイン画面でユーザ名を入れると入れる(**ユーザ名が空でなければ認証する**実装)。
+### 毎回 publish とイメージ ビルドが走る
+
+**`1_PublishAndUp.bat` は、起動のたびに `publish` を作り直す。**
+
+```
+if exist ".\publish" rmdir /s /q ".\publish"
+dotnet publish ... -o ".\publish"
+docker compose up --build -d
+```
+
+意図的である。**ソースとコンテナの内容が必ず一致する**ことを保証している。
+
+実測でも重くない。
+
+```
+dotnet publish 7 秒ほど (obj\ は消さないので増分ビルドが効く)
+docker compose up 6 秒ほど (ベース イメージと RUN 層はキャッシュが効く)
+```
+
+> **`rmdir` をやめてはいけない。**
+> 出力先を消さずに publish すると、**ソースから消したファイルが残り続ける。**
+> 古い DLL を抱えたまま動き、原因の分かりにくい不具合になる。
+
+> **「起動だけ」の口を足すのも見送っている。**
+> 古い `publish` のままコンテナが立ち上がると、
+> **直したはずのものが直っていない**という形で時間を失う。
+> 実際、`#578` では古いビルド成果物のまま測って原因を見誤った。
+
## ファイル構成
| | 役割 |