SAML 2.0によるSSOの仕組みと設計の落とし穴。アサーション検証から署名鍵の保護まで
対象の目安: SSOを設計・レビューする開発者と情シス / 実務

社内で使うSaaSが増えるほど、ログインを1か所にまとめたいという要求が強くなります。その受け皿として長く使われているのがSAML 2.0で、仕様はOASISが2005年3月に標準として公開しました。20年以上前の仕様ですが、企業向けSaaSの管理画面には今もSAML設定の欄が並びます。
仕組み自体は単純です。認証を担当するIdPが、利用者について書いた文書に署名を付け、ブラウザ経由でSPに渡します。SPは署名を確認して、書かれている人物としてセッションを作ります。単純である分だけ、SPの確認が1つ抜けたときの結果もはっきりしています。誰かが用意した文書を、正規の利用者のものとして受け入れてしまいます。
この記事では、SAML 2.0のWeb Browser SSOプロファイルを対象に、メッセージがどう流れるか、署名検証が何を守っているか、SPが確認すべき項目は何かを、OASISの仕様書とOWASPとNISTの記述に沿って並べます。攻撃手法は成立条件を機構として説明するにとどめ、再現手順は扱いません。想定読者は、これからSSOを設計する開発者と、稼働中のSSO設定をレビューする情シスやセキュリティ担当です。
SAML 2.0のSSOに登場する三者と用語
SAMLの登場人物は3つです。認証される人物であるプリンシパル、その人物について言明を作るIdP(Identity Provider)、その言明を受け取って自分のサービスへのアクセスを許すSP(Service Provider)です。仕様の言葉では、言明を作る側をasserting party、受け取って判断に使う側をrelying partyと呼びます。SSOの場面では、IdPがasserting party、SPがrelying partyになります。
仕様は4つの層に分かれています。この分け方を頭に入れておくと、製品のマニュアルを読むときに迷いにくくなります。
| 層 | 仕様書 | 決めていること |
|---|---|---|
| アサーション | SAML Core | 認証や属性を表すXML文書の構造 |
| プロトコル | SAML Core | リクエストとレスポンスの型と処理規則 |
| バインディング | SAML Bindings | メッセージをHTTPにどう載せるか |
| プロファイル | SAML Profiles | 特定の用途で層をどう組み合わせるか |
Web Browser SSOプロファイルは、この4層を「ブラウザを仲介役にしてIdPからSPへ認証アサーションを届ける」形に組み合わせたものです。設計の議論で「SAMLの設定」と呼ばれるものの大半は、このプロファイルの設定を指します。
やり取りする相手の情報は、メタデータというXML文書で表します。メタデータには、相手のentityID(連携相手を一意に識別するURI)、対応するバインディング、エンドポイントのURL、署名や暗号化に使う鍵が入ります。SP側のエンドポイントのうち、アサーションを受け取る場所がAssertion Consumer Service(ACS)です。IdP側で認証要求を受ける場所がSingle Sign-On Serviceです。
SP起点のSSOで流れるメッセージ
SP起点(SP-initiated)の流れは、Web Browser SSOプロファイルの中心です。仕様は6段階で記述しています。
- 利用者のブラウザが、SPの保護されたリソースにセキュリティコンテキストなしでアクセスします。
- SPが、自分の希望するバインディングに対応したIdPのエンドポイントを特定します。特定の方法は実装依存で、IdP Discoveryプロファイルを使うこともできます。
- SPが
<samlp:AuthnRequest>を発行し、ブラウザ経由でIdPへ届けます。ここではRedirect、POST、Artifactのいずれのバインディングも使えます。 - IdPがプリンシパルを識別します。新たな認証を求めることも、既存の認証済みセッションを再利用することもあります。この段階はプロファイルの範囲外です。
- IdPが
<samlp:Response>を発行し、ブラウザ経由でSPへ届けます。ここで使えるのはPOSTかArtifactで、Redirectバインディングは使えません。レスポンスの長さがブラウザのURL長の上限を超えるためです。 - SPがレスポンスを処理し、自分のセキュリティコンテキストを作って、要求されたリソースを返します。
仕様は続けて、IdPがこのプロファイルを段階5から開始し、先行するやり取りなしにSPへ<Response>を送ることもできる、と書いています。これがIdP起点(IdP-initiated)のSSOで、後の節で扱います。
段階4がプロファイルの範囲外である点は、設計で効いてきます。多要素認証をどう要求するかはIdP側の話であり、SAMLのメッセージには認証方法を表す<saml:AuthnContextClassRef>という形でしか現れません。SP側で条件を付けたい場合は<samlp:AuthnRequest>の<samlp:RequestedAuthnContext>で要求し、返ってきたアサーションの<saml:AuthnContextClassRef>を確認します。IdPは要求を無視できるため、結果を見る処理を省くと要求は表明しただけになります。
RedirectバインディングとPOSTバインディングの違い
2つのバインディングは、同じメッセージを違う方法でHTTPに載せます。設定画面で選ばされる場面が多いので、何が変わるのかを押さえておきます。
| 項目 | HTTP Redirect | HTTP POST |
|---|---|---|
| 運び方 | URLのクエリ文字列 | HTMLフォームの隠しフィールド |
| エンコード | DEFLATE圧縮してbase64、さらにURLエンコード | XMLをbase64 |
| パラメータ名 | SAMLRequestまたはSAMLResponse | SAMLRequestまたはSAMLResponse |
| 署名の載せ方 | クエリパラメータSigAlgとSignature | XML署名を文書内に埋め込む |
| 主な用途 | 認証要求の送信 | アサーションを含むレスポンスの配送 |
Redirectバインディングの署名は、XML署名ではなくクエリ文字列に対する署名です。署名対象は、RelayStateがあればSAMLRequest=値&RelayState=値&SigAlg=値の順に連結した文字列で、各値はURLエンコード済みのものを使います。検証側で注意が要るのは、パラメータの並び順がURL上では規定されていない点と、URLエンコードが一意ではない点です。仕様は、検証前に署名規則の順に並べ替えること、そして受け取った元のURLエンコード済みの値をそのまま使って検証することを求めています。いったんデコードしてから再エンコードすると、署名者のエンコードと一致しない可能性があります。
POSTバインディングでは、XMLをbase64にしてSAMLResponseという名前の隠しフィールドに入れ、method="POST"のフォームでACS URLへ送ります。受信側は、フォームがどう送信されたかに関係なくメッセージを処理できなければなりません。
RelayStateはどちらのバインディングでも使えます。仕様は値の長さを80バイト以下に制限し、送信側が独立に完全性を保護すべきだとしています。空間の制約から署名は現実的ではないため、チェックサムや擬似乱数のような手段で改ざんの有無を確認できるようにする、という書き方です。また、リクエストにRelayStateが伴っていた場合、応答側は受け取ったのと同一の値を対応するパラメータに入れて返さなければなりません。
80バイトという制限は、設計上の指針として読めます。ここに遷移先のURLをそのまま入れる設計は、長さの面でも安全性の面でも仕様の想定から外れます。
アサーションの署名検証が守っているもの
SPがアサーションの中身を信じてよい理由は、ただ1つです。その文書がIdPの秘密鍵で署名されていて、SPが持っている検証鍵で確認できたからです。ブラウザを経由して届く以上、経路上の誰もが中身を読み書きできる前提に立つ必要があり、TLSは経路を守りますが、ブラウザ自身が中継点である以上、メッセージの真正性は署名でしか担保できません。
SAMLはXML署名の使い方を絞っています。アサーションとプロトコルメッセージには囲い込み型(enveloped)の署名を使うこと、署名対象のルート要素にはID属性を必ず付けること、<ds:Signature>は署名対象のルート要素のID値を指す同一文書参照を持つ<ds:Reference>をちょうど1つ含むこと、という決まりです。IDがfooなら<ds:Reference>のURI属性は#fooになります。正準化はExclusive Canonicalizationを使うことが推奨され、変換は囲い込み署名の変換以外を含めないほうがよいとされています。
問題は、この決まりを守った文書だけがSPに届くわけではない、という点です。XML署名は「文書のどこかにある要素に対する署名」を表現でき、署名検証の処理は「有効な署名が存在するか」を答えるだけになりがちです。一方でアプリケーション側は、NameIDや属性の値を、署名検証とは別の経路で文書から読み出します。この2つの処理が別々の見方で同じ文書を扱うと、両者がずれます。
XML Signature Wrapping(XSW)は、このずれを突く攻撃の総称です。成立には次の条件が重なります。
- 署名検証の処理が、署名が有効かどうかだけを返し、どの要素が署名されていたかを呼び出し側に伝えていません。
- 値の読み取りが、タグ名検索のように署名対象と独立した経路で行われ、文書内の別の位置にある同名要素を拾いえます。
- スキーマ検証が行われないか、拡張点が緩く、余分な要素を差し込んでも構造として通ってしまいます。
2012年のUSENIX Securityで発表された「On Breaking SAML: Be Whoever You Want to Be」は、14のSAMLフレームワークとシステムを解析し、そのうち11で重大なXSWの脆弱性を確認したと報告しています。著者らは原因を2点に整理していて、1つはXML署名の仕様がPKCS#7のような従来形式より複雑で、署名の位置と署名対象の位置が可変であること、もう1つは署名検証モジュールとアサーション処理モジュールが別々の文書の見方を持ちながら、真偽値だけをやり取りしている点です。
同じ論文が示す対策は、実装のレビュー観点としてそのまま使えます。1つは「ハッシュされたものだけを処理する」考え方で、署名検証モジュールが検証済みの部分だけを後続へ渡します。もう1つは「署名されたものが見える」ようにする考え方で、検証時に署名対象の要素へ印を付け、処理側はその印がある要素からしか値を読みません。どちらも、検証した文書と処理する文書を同一に保つ一点に集約されます。
OWASPのSAML Security Cheat Sheetは、実装寄りの推奨として次を挙げています。セキュリティ用途で使う前に必ずスキーマ検証を行い、スキーマはローカルの信頼できるコピーを使い、第三者からの自動ダウンロードは許さないこと。署名鍵は文書内の<ds:KeyInfo>を無視してIdPから直接得たものを使うこと。そして、事前検証なしにgetElementsByTagNameのようなタグ名検索でセキュリティ関連の要素を選ばず、硬化したスキーマで検証していない限り絶対XPathで要素を選ぶことです。
読み取り経路のずれは、要素の選び方だけでなくテキストの読み方でも起きます。CERT/CCが2018年2月に公開したVU#475445は、複数のSAMLライブラリで、XMLのコメントを含むノードの内側テキストの扱いがずれ、署名の検証は通ったままアプリケーションが読み取る識別子の値だけが変わる状態になりうると報告しています。この種の差異を自作の解析処理で全部潰すのは現実的ではありません。
署名対象がアサーションかレスポンスかで変わること
製品の設定画面には「レスポンスに署名する」「アサーションに署名する」が別々のチェックボックスとして並ぶことがよくあります。この2つは守る範囲が違います。
POSTバインディングで<Response>を配送する場合、プロファイル仕様は中に入るアサーションへの署名を必須としています。レスポンス自体への署名は必須ではありません。ここから、実際に起こりうる組み合わせが3つ出てきます。
<samlp:Response ID="_r1" Destination="https://sp.example.com/acs" InResponseTo="_q1">
<saml:Issuer>https://idp.example.com/</saml:Issuer>
<!-- (A) ここに署名が付くとレスポンス全体が保護される -->
<samlp:Status><samlp:StatusCode Value="...:status:Success"/></samlp:Status>
<saml:Assertion ID="_a1">
<saml:Issuer>https://idp.example.com/</saml:Issuer>
<!-- (B) ここに署名が付くとアサーションだけが保護される -->
<saml:Subject>
<saml:NameID>user@example.com</saml:NameID>
<saml:SubjectConfirmation Method="...:cm:bearer">
<saml:SubjectConfirmationData Recipient="https://sp.example.com/acs"
NotOnOrAfter="2026-09-08T09:05:00Z" InResponseTo="_q1"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="..." NotOnOrAfter="...">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com/metadata</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="..." SessionIndex="..." SessionNotOnOrAfter="..."/>
</saml:Assertion>
</samlp:Response>
アサーションだけに署名がある場合、<samlp:Response>側のDestinationや<samlp:Status>は署名の外にあります。これらは改ざんされうる値として扱い、セキュリティ判断の材料にする際は、その値が署名で保護されていないことを踏まえた使い方をします。Destinationの照合は、署名対象かどうかにかかわらず、別の宛先へ送られたメッセージの流用を弾くために必要です。
レスポンスだけに署名がある場合は、アサーション単体を取り出して別の場所へ持ち込まれても検出できるかが論点になります。POSTバインディングを使う限り、プロファイル仕様はアサーションへの署名を求めているので、まずはその要求を満たしているかを確認します。
いずれの場合でも、SP側でやるべき確認は同じ一文に集約されます。自分が値を読み取る要素が、検証に成功した署名の対象に含まれているかどうかです。OWASPのチェックリストも、XML署名の<ds:Reference URI>が信頼しようとしている<saml:Assertion>要素を覆っていることを確認する、という項目を挙げています。
暗号化アサーション(<saml:EncryptedAssertion>)を使う場合は、復号してから署名検証を行います。SPの公開鍵は公開情報なので誰でも暗号化でき、復号できたことは送信者の真正性を意味しません。
SPが検証すべき属性と、それぞれが止めるもの
プロファイル仕様は、バインディングによらずSPが行うべき処理を列挙しています。アサーションかレスポンスに付いた署名の検証、bearerの<saml:SubjectConfirmationData>のRecipientがアサーションを受け取ったACS URLと一致することの確認、同じ要素のNotOnOrAfterが過ぎていないことの確認(プロバイダ間の時刻ずれは許容範囲を設ける)、InResponseToが自分が出した<samlp:AuthnRequest>のIDと一致することの確認です。未承諾レスポンスの場合はInResponseToが存在してはならない、という条件付きです。加えて、条件を満たさないアサーションは破棄し、セキュリティコンテキストの確立に使わないこと、SessionNotOnOrAfterが付いていればその時刻でコンテキストを捨てることも書かれています。
DestinationはCore仕様の側で定義されています。メッセージが送られた宛先を表すURI参照で、意図しない受信者への悪意ある転送を防ぐためのものです。存在する場合、実際の受信者はそのURI参照がメッセージを受け取った場所を指しているか確認しなければならず、一致しなければメッセージを破棄しなければなりません。
<saml:Audience>は、そのアサーションが誰宛かを表します。Core仕様は、聴衆制限条件はrelying partyが指定された聴衆の1つに含まれるときにのみ有効と評価される、と定義しています。SPが自分のentityIDと照合しないと、他社SP向けに発行された正規のアサーションをそのまま受け入れる状態になります。
まとめると、次の表になります。
| 確認対象 | 位置 | 確認内容 | 抜けたときに通るもの |
|---|---|---|---|
<ds:Signature> | Response/Assertion | 期待するIdPの鍵で検証し、<ds:Reference URI>が信頼する要素を覆う | 偽造アサーションとXSW |
<saml:Issuer> | Response/Assertion | 期待するIdPのentityIDと一致 | 別IdPが発行したアサーション |
Destination | Response | ACS URLと完全一致 | 別のSPや別エンドポイント宛の転用 |
<samlp:StatusCode> | Response | Successであること | エラー応答をログイン成功として処理 |
<saml:Audience> | Assertion | 自SPのentityIDと一致 | 他SP向けアサーションの持ち込み |
NotBefore/NotOnOrAfter | Conditions | 現在時刻が範囲内(時刻ずれを許容) | 期限切れアサーションの再利用 |
Recipient | SubjectConfirmationData | ACS URLと一致 | 配送先のすり替え |
NotOnOrAfter | SubjectConfirmationData | 配送猶予内であること | 時間をおいた持ち込み |
InResponseTo | Response/SubjectConfirmationData | 自分が出した要求のIDと一致 | 別セッションのアサーション注入 |
アサーションのID | Assertion | 使用済み集合に無いこと | リプレイ |
<saml:AuthnContextClassRef> | AuthnStatement | 要求した認証水準を満たす | 想定より弱い認証の受け入れ |
SessionNotOnOrAfter | AuthnStatement | セッション上限に反映 | 想定より長いSP側セッション |
レビュー時にそのまま使える形に落とすと、次の項目になります。
SPが検証すべき項目
- レスポンスのDestination属性が自SPのACS URLと完全一致し、属性が存在しないレスポンスを既定で拒否しているか
- 署名の検証鍵をIdPのメタデータまたは事前に交換した証明書から取得し、文書内のds:KeyInfoを信用していないか
- XML署名のds:Reference URIが、実際に値を読み取るsaml:Assertion要素を覆っていることを確認しているか
- 署名アルゴリズムをRSA-SHA-256以上に限定し、SHA-1系のSignatureMethodとDigestMethodを拒否しているか
- 署名が無いアサーション、および署名検証に失敗したアサーションを破棄しているか
- saml:Issuerが期待するIdPのentityIDと一致することを確認しているか
- saml:Audienceが自SPのentityIDと一致することを確認しているか
- saml:ConditionsのNotBeforeとNotOnOrAfterを、許容する時刻ずれの範囲を明示したうえで検証しているか
- bearerのsaml:SubjectConfirmationDataのRecipientがACS URLと一致することを確認しているか
- 同じ要素のNotOnOrAfterを検証し、NotBeforeが含まれていないことを確認しているか
- InResponseToが自分の発行したAuthnRequestのIDと一致することを確認しているか
- 未承諾レスポンスを受け付ける設定の場合に、InResponseToが存在しないことを確認しているか
- samlp:StatusCodeがSuccessであることを確認したうえでアサーションを処理しているか
- 使用済みアサーションのIDを有効期間のあいだ保持し、再提示を拒否しているか
- XMLパーサでDTDと外部実体参照を無効にしているか
- スキーマ検証をローカルに保存したスキーマで行い、外部からのスキーマ取得を行わない設定か
- 値の読み取りにタグ名検索を使わず、検証済みの署名対象から読んでいるか
- RelayStateをURLとして扱う場合に、許可リストで検証してから遷移しているか
- SessionNotOnOrAfterを受け取ったときにSP側セッションの上限へ反映しているか
- IdPのメタデータをvalidUntilとcacheDurationの範囲で更新し、署名を検証しているか
リプレイ防止に必要な状態管理
署名の検証だけでは、同じアサーションを2回使われることを防げません。プロファイル仕様は、POSTバインディングを使う場合の追加規則として、SPがbearerアサーションのリプレイを防がなければならないと定め、その方法として<saml:SubjectConfirmationData>のNotOnOrAfterに基づく有効期間のあいだ、使用済みのID値の集合を保持することを挙げています。
実装で見落としやすいのは、この集合をどこに置くかです。SPが複数インスタンスで動いている場合、プロセス内のメモリに持つと、別インスタンスへ同じアサーションを投げれば通ってしまいます。共有ストアに置くか、ロードバランサで同一インスタンスに固定するかの判断が要ります。保持期間はアサーションの有効期間で決まるため、NotOnOrAfterを短くしておけばストアの負荷も下がります。
Core仕様には<saml:OneTimeUse>という条件要素もあります。アサーションを即座に使い保持してはならないことを示すもので、有効期間の指定とは独立した条件です。OWASPも、ブラウザの履歴やキャッシュからの露出とリプレイへの対策として、レスポンスの寿命を短くすることとOneTimeUseを挙げています。ただしSP側がこの条件を解釈しなければ効果はありません。
NIST SP 800-63C-4は、リプレイ防止をFAL1(最も基礎的なフェデレーション保証レベル)の要求として置いています。アサーションは特定のRPまたはRPの集合に対して聴衆制限され、RPは自分が対象に含まれることを検証しなければならず、聴衆に含まれる各RPは同じアサーションが複数回受け入れられないようリプレイ防止の機構を強制しなければならない、という書き方です。FAL2では聴衆制限が単一のRPに絞られます。
時刻ずれの扱いも決めておきます。プロファイル仕様はNotOnOrAfterの検証を「プロバイダ間の許容できる時刻ずれを考慮して」と書くにとどめ、具体的な秒数は示していません。許容幅を広く取るほどリプレイの窓が広がるため、双方をNTPで同期させたうえで数十秒程度に抑えます。
セッションの寿命そのものの考え方は、SSOに限らない話とつながります。
あわせて読みたい
セッション管理とセッションハイジャック対策。Cookie属性とセッションID再生成で不正利用を防ぐ設計
IdP起点のSSOが持ち込む前提
IdPのポータルにSaaSのタイルが並んでいて、クリックすると直接そのSaaSにログインできる、という体験はIdP起点のSSOで実現されます。プロファイル仕様はこれを「未承諾レスポンス」と呼び、IdPがSPへ<Response>を届けることでプロファイルを開始できると定めています。未承諾レスポンスはInResponseTo属性を含んではならず、bearerの<saml:SubjectConfirmationData>にも含めるべきではありません。メタデータを使う場合は、既定として指定された<md:AssertionConsumerService>エンドポイントへ届けることが推奨されます。
ここで失われるものは、SP側が「このログインは利用者が意図して始めたものか」を判断する材料です。SP起点であればSPが事前にセッションを作り、自分が出した要求のIDと突き合わせられます。未承諾レスポンスではその手がかりがありません。OWASPは、この設計がログインのCSRF対策を欠く点を指摘し、SPが事前ログインセッションを作ったり利用者が意図して認証要求を開始したかを検証したりする機会を持てないことを理由として挙げています。そのうえで、SAML 1.1との後方互換のために残されている機能であり、有効にするなら4.1.5の検証手順に従い、RelayStateがURLであれば許可リストで検証し、レスポンスかアサーションの水準でリプレイ検知を実装すべきだとしています。
NIST SP 800-63C-4は、この論点をアサーション注入攻撃として整理しています。攻撃者がRPにアサーションやアサーション参照を処理させ、RPが攻撃者のセッションをアサーション内の識別子に結び付けてしまう形の攻撃です。文書は、攻撃対象面を狭める一般的な実践の1つとして、IdP起点のフェデレーション処理を禁止し、RPが未承諾のアサーションを受け入れないようにすることを挙げています。ただしこの禁止には、外部の主体がRPに対して「IdPとのフェデレーションを開始せよ」と合図し、RPが自らトランザクションを開始してその中で応答を待つ形は含まれない、という但し書きが付いています。
つまり、IdPのポータルからの導線を残したまま安全性を上げる方法があります。IdPのタイルのリンク先をSPのACSではなく、SP側に用意したログイン開始エンドポイントにします。SPはそこで自分のセッションを作り、<samlp:AuthnRequest>を発行してSP起点の流れに合流します。利用者の体験は変わらず、InResponseToの照合が使えるようになります。NIST SP 800-63C-4はFAL2の要求として、フェデレーショントランザクションがRPによって開始されなければならないと明記しているため、この形はFAL2を目指す場合の前提でもあります。
既存の連携でIdP起点をやめられない場合は、代わりに何を積むかを決めます。未承諾レスポンスに対しては、InResponseToが無いことの確認、DestinationとRecipientとAudienceの厳密な照合、使用済みIDによるリプレイ拒否、RelayStateの許可リスト検証が、実質的な防御線になります。
RelayStateがオープンリダイレクトになる条件
RelayStateは仕様上、SPが保持する状態への不透明な参照です。ところが実装では、ログイン後に戻る先のURLをそのまま入れる使い方が広く行われています。この形にすると、次の条件がそろったときにオープンリダイレクトが成立します。
- SSOの開始URLやIdPのエンドポイントに、外部から
RelayStateの値を指定できます。 - SPがログイン成功後に
RelayStateの値をそのまま遷移先として使います。 - 遷移先の検証が無いか、ホスト名の部分一致など回避しやすい方法で行われています。
利用者から見ると、自社のIdPで正規にログインした直後に外部サイトへ移動することになります。フィッシングの導線として使われやすいのは、この「正規ドメインを経由する」性質です。
対策は2通りです。1つは仕様の意図どおりに使う方法で、RelayStateには推測できないキーだけを入れ、実際の遷移先はSP側のサーバに保持します。80バイトの制限にも自然に収まります。もう1つは、URLを入れる運用を続けたうえで、遷移前に許可リストで検証する方法です。検証はスキームとホストとパス接頭辞を組にして行い、相対パスのみを許す場合も//example.invalidのようなスキーム相対の書き方が絶対URLとして解釈されることを踏まえた実装にします。
仕様がRelayStateの完全性保護を求めている点も設計に反映できます。SP側でHMACなどの検証子を付けておけば書き換えを検出できますが、80バイトの制限は変わらないため、不透明なキーにするほうが素直です。
署名鍵と証明書とメタデータの運用
SAMLの信頼関係は、SPがIdPの署名証明書を明示的に信頼することで作られます。ここが弱いと、他のすべての検証が意味を失います。
メタデータ仕様は、メタデータ文書の後処理について次を定めています。文書のキャッシュは対象要素のvalidUntilまたはcacheDurationを超えてはならず、cacheDurationを正しく扱うには取得日時を保持しなければなりません。期限が切れたら新しいコピーを取得しなければなりません。後処理はXML文書水準の署名処理を必ず含み、DNSゾーン署名かTLSサーバ認証のいずれかを加えてもよい、という構成です。公開側は文書の完全性機構を必ず用いなければならず、消費側は署名が存在すれば必ず検証しなければなりません。
運用としては、証明書をファイルで受け渡すのではなく、IdPのメタデータURLをSPに登録して自動更新させる形が扱いやすくなります。OWASPも、IdPとSPの関係の理想形はメタデータURLの利用であり、そのURLは広く信頼されたWebPKIのCAが発行したサーバ証明書によるTLSで保護すべきだとしています。使わない場合は、攻撃者がSPに誤った証明書を信頼させないよう、証明書をメールで送ることを避け、適切に構成されたTLSで提示するよう求めています。
証明書そのものの要件も、OWASPの記述に具体的な数字があります。鍵はRSAなら最低2048ビット、ハッシュはSHA-256が最低線で、SAML署名証明書の有効期間の上限は2年にすべきだとしています。秘密鍵がHSMのような形で十分に保護されていない場合、2年でも長すぎる可能性があるという但し書きも付いています。用途の分離についても、鍵用途はdigitalSignatureに限り、IdPのTLSサーバ証明書をSAML署名証明書として使ってはならないとしています。
ローテーションの手順は、事前に決めておかないと当日に止まります。メタデータには署名用途の<md:KeyDescriptor>を複数並べられるため、新しい鍵を追加してSP側に取り込ませる期間を作り、切り替えたあとに旧鍵を外す順序にします。自動更新しているSPでもcacheDurationの分だけ反映が遅れることを見込み、自動更新に対応していないSPは個別に連絡する対象として一覧にしておきます。
NIST SP 800-63C-4も、識別子と鍵の更新プロセスは信頼合意で定義され、初回の確立と同様に認証済みの保護された通信路で実行されなければならないとしています。
Golden SAMLで起きることと、鍵の保護
SPがアサーションを信じる根拠は「IdPの鍵で署名されている」という一点でした。裏を返せば、その鍵を手に入れた側は、任意のNameIDと任意の属性と任意の有効期間を持つアサーションを作れます。IdP側で認証イベントは発生しないので、IdPに設定した多要素認証もパスワードポリシーも通過しません。SPから見ると、正規のアサーションと区別が付きません。この形の攻撃はGolden SAMLと呼ばれます。
MITRE ATT&CKはこれをT1606.002(Forge Web Credentials: SAML Tokens)として整理しています。有効なSAMLトークン署名証明書を持っていれば任意の権限クレームと寿命でトークンを偽造でき、SAML 2.0をSSOの仕組みとして使うサービス群に対して認証できると説明されています。攻撃者が自分のAD FSサーバとのあいだに新しいフェデレーション信頼を確立できる権限を持つ場合は、自前の信頼された署名証明書を生成する形もあるとされています。
CISAのアドバイザリAA21-008Aは、実際の侵害でこの手法が使われたことを記述しています。攻撃者がAD FSのトークン署名証明書を盗んでSAMLトークンを偽造し、フェデレーション先のリソースプロバイダの多要素認証とパスワードの要求を迂回して、M365環境へ横方向に移動したという説明です。IdP側で検査されないままサービスプロバイダへクレームを発行できてしまう点が、この手法の要になっています。
対策の中心は、署名鍵をファイルとして持たないことです。OWASPは、SAML署名鍵が攻撃者の標的になる最上位の資産であり、ファイルとして置かれた鍵は持ち出しが容易だとして、HSMによる保護を強く検討すべきだとしています。HSMは鍵をエクスポート不可能な状態のままアプリケーションに使わせ、フェイルオーバー用への複製も鍵を外部に出さずに行えます。品質の目安としてFIPS 140-2またはFIPS 140-3の認証が示されています。
NIST SP 800-63C-4も、CSPとIdPとRPがすべての署名鍵と復号鍵と共通鍵を安全に保管しなければならないとし、保管はFIPS 140の要求(耐タンパー性の要求を含む)に従うとしています。非エクスポートとみなされるには、鍵の保管がホストプロセッサから分離された独立したハードウェアか、セキュアエレメントやTEEやTPMのような組み込みプロセッサでなければならず、鍵を取り出せるように再プログラムできてはならない、という要求です。連邦機関が運用するIdPについては、FAL2以上でアサーション生成に使う署名鍵をFIPS 140レベル1以上で検証された機構で保護しなければならないとされています。
封じ込めと検知の考え方も、公開情報から具体化できます。ATT&CKの緩和策は、偽造済みトークンの影響を封じ込めるためにAD FSのトークン署名証明書を短い間隔で2回ローテーションして旧証明書のトークンを無効化すること、AD FSで詳細監査を有効にすること、AD FSサーバへのアクセスを特権アクセスワークステーションからのみに限ることを挙げています。検知の着眼点としては、署名は有効なのに先行するKerberosや認証のイベントが欠けている認証、通常と異なる寿命や発行者やクレームを持つトークン、多要素認証を経ずに成功したSaaSログイン、同一のアサーションIDで地理的に離れた同時セッションが示されています。
注意
Golden SAMLの検知は、SP側のログだけでは成立しません。SPから見れば正規の署名が付いた正常なログインだからです。IdP側の認証イベントとSP側のログイン記録を突き合わせて、「SPではログインが成立しているのにIdP側に対応する認証イベントが無い」という不整合を見つける形になります。SIEMへログを集める段階で、双方のログを同じ時間軸に載せられるようにしておく必要があります。
IdPの署名鍵を守ることは、IdPが動くホストの管理そのものでもあります。鍵を扱うサーバの管理者権限を持つ相手は鍵に到達しうるため、管理経路の分離と特権アカウントの保護が、SAMLの設定と同じ重みを持ちます。
あわせて読みたい
多要素認証(MFA)の選び方と導入の勘所。方式比較から運用まで徹底解説
SAMLとOIDCの使い分け
新規に認証連携を設計するとき、SAMLとOpenID Connect(OIDC)のどちらを選ぶかという問いが出ます。NIST SP 800-63C-4は両者をFALという枠組みで並べていて、判断の材料になります。
同文書の表は、FAL1の例としてSAMLのWeb Browser SSOプロファイル(XML署名によるアサーション署名とフロントチャネルでの提示)を挙げ、OIDCについては主要なフローがJSON Web Signatureによる署名を要求するよう設定できることを挙げています。FAL2の例としては、SAMLのArtifactバインディングによるバックチャネルでのアサーション提示と、OIDCでIDトークンをバックチャネルで提示するフローが、いずれもアサーション注入への一定の保護を与えるとされています。
技術的な差は形式にあります。SAMLはXMLとXML署名を使い、OIDCはJSONとJWSを使います。XML署名は署名の位置と署名対象の位置が可変であることに由来する扱いにくさがあり、JWSはヘッダとペイロードと署名を.で連結した固定の構造を取ります。この違いが、XSWのような攻撃面の有無に直結します。
選択が自由でない場面も多くあります。企業向けSaaSの設定欄にSAMLしか無ければSAMLで組むことになり、新しく作る自社サービスならOIDCを選べます。判断軸としては、相手側の対応状況、属性の受け渡し方法、モバイルアプリやAPIアクセスの有無、運用チームの慣れが実際的です。どちらを選んでも、署名の検証、対象者の確認、有効期間の確認、リプレイの拒否、注入の防止という5点は形式が変わっても残ります。
あわせて読みたい
OAuth 2.0とOpenID Connectの基礎。認可と認証の違いを仕組みから整理する
SLOの限界とセッション寿命の設計
SAMLにはSingle Logout(SLO)プロファイルがあり、1回の操作でIdPと各SPのセッションをまとめて終わらせる形を定義しています。ただし、これが確実に効くと期待した設計は破綻します。
Core仕様は、セッション権威(通常はIdP)が各セッション参加者に対して、適用可能なプロトコルバインディングで連絡を試みるべきだとしつつ、一部が失敗したり試行自体ができなかったりする場合があることを前提にしています。すべての参加者が正常に応答しなかった場合、<LogoutResponse>に第2水準のステータスコードとしてurn:oasis:names:tc:SAML:2.0:status:PartialLogoutを含めなければならない、という定めです。部分的なログアウトが仕様の中で正規の結果として扱われている点が要点です。
Technical Overviewは理由も書いています。ブラウザのログアウト操作はローカルの認証クッキーへのアクセスを必要とすることが多く、利用者のブラウザ操作が処理を中断することもあるため、結果は保証できない、という説明です。フロントチャネルでSPを順に巡る方式では、途中でタブを閉じられれば残りのSPには届きません。
設計としては、次の4つを組み合わせます。SP側のセッション寿命を短くして、ログアウトが届かなくても時間で切れるようにすること。アサーションのSessionNotOnOrAfterを受け取ったらSP側セッションの上限に反映すること。権限の高い操作の直前に再認証を求めること。退職者や事故時の即時遮断をSLOに頼らないこと。最後の1つが実務では効きます。
SCIMによるプロビジョニングと退職者の失効
SSOはログインの経路であって、SP側のアカウントそのものを消す仕組みではありません。IdP側でアカウントを無効化すれば新しいログインは止まりますが、SP側にローカルパスワードが残っていたり、APIトークンが発行されていたり、既存セッションが生きていたりすれば、そこからのアクセスは残ります。
アカウントのライフサイクルを揃えるための標準がSCIM(System for Cross-domain Identity Management)です。RFC 7643がコアスキーマを、RFC 7644がプロトコルを定義しています。IdP側の人事イベントに合わせて、SPのユーザーリソースを作成し、属性を更新し、activeをfalseにする、あるいは削除するという操作をHTTPで行います。
SAMLのJITプロビジョニング(初回ログイン時にSP側アカウントを自動作成する方式)を使っている場合、作る側だけが自動化され、消す側が手作業のまま残りがちです。棚卸しでは、SPごとにアカウントの作成経路(SCIM、JIT、手動)、無効化の経路(SCIM、手動、SP管理画面)、SSO以外のログイン経路の有無(ローカルパスワード、APIキー、個人アクセストークン)を表にします。3列目に印が付いたSPが、IdP側の無効化だけでは遮断できない対象です。
あわせて読みたい
入社から退職までアカウントを人事イベントに同期させる運用の作り方
実装で自作を避けるべき部分
SAMLのSPを実装するとき、XML署名の検証処理を自作するのは避けるのが無難です。正準化と参照解決と文書構造の組み合わせが多く、検証した文書と処理する文書の同一性を保つ実装を最初から正しく書くのが難しいためです。署名検証と値の読み取りが別々の見方を持った時点で、XSWの成立条件がそろいます。
実績のあるライブラリやIdP製品のSPコンポーネントを使ったうえで、次を確認します。
- 署名が無いアサーションを既定で拒否するか。設定で緩められる場合、その設定が現在どうなっているか。
- 検証鍵をメタデータや設定から取り、文書内の
<ds:KeyInfo>を信用しない挙動か。 AudienceとDestinationとInResponseToの検証が既定で有効か。個別のスイッチがある製品では、現在値を読み出したか。- 許容する署名アルゴリズムを明示的に設定できるか。SHA-1系を拒否できるか。
- 更新が継続していて、過去の脆弱性への対応履歴を追えるか。
XMLパーサの設定も見ます。SAMLメッセージはXMLなので、XML外部実体参照(XXE)の対象になります。OWASPのXXE防止チートシートは、XXEを防ぐ最も安全な方法はDTD(外部実体)を完全に無効化することだとしています。Javaではhttp://apache.org/xml/features/disallow-doctype-declをtrueに設定し、あわせてXMLConstants.FEATURE_SECURE_PROCESSINGを有効にします。.NETではXmlReaderSettingsのDtdProcessingをProhibitにします。Pythonではdefusedxmlパッケージを使う方法が示されています。
スキーマ検証も、行うなら取得元を固定します。OWASPの推奨は、検証に使うスキーマは常にローカルの信頼できるコピーを使い、第三者の場所からの自動ダウンロードを許さないことです。取得先がネットワーク越しだと、そこが可用性の単一障害点になり、同時に信頼の起点にもなります。なお、属性文で受け取った表示名や部署名をそのまま画面に出す処理は、通常のXSS対策の対象です。
あわせて読みたい
認証と認可の違い。Authentication と Authorization を基礎から整理する
既存SSOの点検手順
稼働中のSSOをレビューする順序です。設定を変える前に、現在の状態を書き出す作業から始めます。
- 1
連携の一覧とメタデータの所在を洗い出す
IdPに登録されているSPと、各SPに登録されているIdPの情報を突き合わせます。entityID、ACS URL、メタデータの取得方法(URLか手動登録か)、署名証明書の指紋と有効期限を1つの表にします。ここで「誰も知らないSP」が出てくることがあります。
- 2
SP側で有効になっている検証項目を読み出す
製品ごとに名前は違いますが、署名の検証、Audienceの検証、Destinationの検証、InResponseToの検証、リプレイ検知、許容する時刻ずれに相当する設定を探して現在値を記録します。既定値のままか、過去に緩められているかを見ます。
- 3
署名対象がどこかを実際のメッセージで確認する
テスト環境で1回SSOを実行し、レスポンスのXMLを取得して、署名がレスポンスとアサーションのどちらに付いているか、
<ds:Reference URI>がどのIDを指しているかを目視します。設定名だけでは分からない部分がここで確定します。 - 4
IdP起点のSSOが必要かを判断する
IdPのポータルからの導線が実際に使われているかを確認します。不要なら未承諾レスポンスの受け入れを止めます。必要なら、IdPのタイルの遷移先をSP側のログイン開始エンドポイントに変えられるかを検討します。
- 5
RelayStateの扱いを追う
RelayStateに何が入り、SPがログイン後にそれをどう使うかをコードか設定で確認します。遷移先URLを直接入れている場合は、許可リスト検証の有無と、その検証がスキーム相対の書き方を弾けるかを見ます。
- 6
署名鍵の保管場所とローテーション計画を確認する
IdPの署名鍵がファイルとして置かれているか、HSMなどエクスポートできない形で保管されているかを確認します。証明書の有効期限を一覧にし、更新の何日前に誰が何をするかを決めます。新旧の鍵を並べる期間と、自動更新に対応していないSPの連絡先も書き出します。
- 7
ログと突き合わせの経路を確認する
IdPの認証イベントとSPのログイン記録が同じ基盤に集まっているかを見ます。集まっていない場合、SPのログイン成功に対応するIdPの認証イベントが存在するかを後から確認できる状態を作ります。
- 8
退職時と事故時の遮断手順を確認する
IdP側の無効化だけで各SPへのアクセスが止まるかをSPごとに判定します。止まらないSPについては、SCIMによる失効か手動手順を退職フローに組み込みます。SLOに依存した手順になっていないかも見ます。
点検の結果を残すときは、設定値に「なぜその値なのか」を1行添えておきます。時刻ずれの許容幅やセッション寿命は、根拠が書かれていないと運用の都合で広げられがちな値です。
まとめ
SAML 2.0のSSOは、IdPが署名した文書をSPが検証するという単純な構造の上に立っています。単純だからこそ、SPが行う検証の一覧がそのまま安全性の一覧になります。OASISのプロファイル仕様が求めるのは、署名の検証、RecipientとACS URLの一致、NotOnOrAfterの未経過、InResponseToと自分の要求のIDの一致、そしてPOSTバインディングでは使用済みIDの保持によるリプレイ拒否です。Core仕様はDestinationの一致確認と、署名が署名対象ルート要素のIDをちょうど1つ参照することを定めています。これらは設定画面のチェックボックスに対応しているので、レビューでは現在値を1つずつ読み出す作業になります。
設計の判断が要るのは3か所です。IdP起点のSSOを受け入れるかどうか。RelayStateに何を入れるか。署名鍵をどこに置くか。1つ目と2つ目は、SP起点への合流と不透明なキーの採用で解消できます。3つ目は、鍵をファイルとして持たない形へ移す判断と、鍵の使用を前提にしたログの突き合わせを用意する判断の両方を含みます。IdPの署名鍵を持つ側は任意の利用者になりすませるため、そこだけは検証項目では止められません。
SLOとプロビジョニングは、SSOの設定とは別の経路として扱います。ログアウトの結果は仕様の水準で保証されておらず、SP側のアカウントはSSOを止めても残ります。セッション寿命の短縮とSCIMなどによる失効の経路づくりが、設定画面の外側で必要になります。
出典・参考
- OASIS Assertions and Protocols for SAML V2.0 (SAML Core)
- OASIS Bindings for SAML V2.0 (SAML Bindings)
- OASIS Profiles for SAML V2.0 (SAML Profiles)
- OASIS Metadata for SAML V2.0
- OASIS Security and Privacy Considerations for SAML V2.0
- OWASP SAML Security Cheat Sheet
- NIST SP 800-63C-4 Digital Identity Guidelines: Federation and Assertions
- MITRE ATT&CK T1606.002 Forge Web Credentials: SAML Tokens
- CISA AA21-008A Detecting Post-Compromise Threat Activity in Microsoft Cloud Environments
関連する記事
OAuth 2.0とOpenID Connectの基礎。認可と認証の違いを仕組みから整理する
SNSログインや「〜で続ける」の裏側にあるOAuth 2.0とOpenID Connectを一次仕様から整理します。OAuthは認可、OIDCはその上に載る認証という役割の違い、登場人物、認可コードフローとPKCE、アクセストークンとリフレッシュトークンとIDトークンの違い、Implicitフローが非推奨になった理由と典型的な落とし穴まで、開発者と実務者向けに解説します。
認証と認可の違い。Authentication と Authorization を基礎から整理する
認証(本人確認)と認可(権限付与)は似た言葉ですが役割は別物です。両者の違い、混同が招くアクセス制御の不備、IDトークンとアクセストークンの使い分け、OAuth2.0とOpenIDConnectの関係までを入門者向けに具体例で整理します。
セッション管理とセッションハイジャック対策。Cookie属性とセッションID再生成で不正利用を防ぐ設計
WebアプリのセッションIDを安全に扱う設計を、生成のエントロピー、HttpOnlyやSecureなどのCookie属性、ログイン時の再生成、寿命管理まで、なぜその設定が効くのかという機構から実務目線で整理します。


