SAMLの証明書更新のお話
Digital Identity技術勉強会 #iddanceのカレンダー | Advent Calendar 2021 10日目の記事です。
今回は、SAMLによる認証連携を設定して運用を開始し安定稼働している中で、忘れたころにやってくる証明書更新 (ローテーションやロールオーバーとも呼ばれます) のお話です。
SAMLの証明書
SAMLそのものの話を書くと長くなりそうなので、ここではざっくり説明となりますが、SAMLではIdPとSPの間でX.509証明書を含むメタデータを交換し、認証連携時にその証明書に含まれる公開鍵で署名検証を行うことで、IdP-SP間でやりとりされるメッセージが改ざんされていないことを確認します。
また、この証明書は
- IdP側の証明書 (SP側に設定する)
- SP側の証明書 (IdP側に設定する)
の2つがあります。よくあるSSOのユースケースにおいては、特にIdP側の証明書を使うことになります。こちらはIdP => SPに対して送信されるSAMLレスポンス(アサーション)に付けられる署名の検証に使われます(OIDCで言うと、IDトークンの署名検証を行うイメージですね)。よって、ブラウザ経由でやり取りすることが多いSAMLでは、改ざん防止によるなりすましログイン防止のため、署名検証は必須となります。
一方、SP側の証明書はSP => IdPに対して送信されるSAMLリクエストなどに付けられる署名の検証に使われます(OIDCで言うと、リクエストオブジェクトの署名)。ただし、SAMLリクエストの署名はSPによっては必須だったりオプションだったり、そもそも署名機能なし、とサービスによってバラバラです。自分の感覚的には、こちらは署名なしのケースが多いかな?という感じです。
以降では、基本的に利用が必須であるIdP側の証明書の更新にフォーカスした内容になります。
証明書の更新
定期的、またはセキュリティ侵害を検知した場合は即座に証明書を更新することが望ましいとされています。セキュリティ侵害の検知は分かりやすいですが、定期的にも行うべきとされています。ここらへんの詳しい話は、鍵管理の推奨事項をまとめた NIST SP 800-57 Part 1 / Recommendation for Key Management を読むとよいかもしれません (日本語翻訳版もIPAで公開されています)。
このNISTのドキュメントでは、認証目的で使われる鍵(認証プライベート鍵/認証公開鍵)は1〜2年で更新することが推奨されていますが、実際はIdPによって様々です。例えば、Azure ADが発行するIdP側の証明書はデフォルトだと3年になっています[1]。サービスやプロダクトによってデフォルトの長さは違いますが、数年単位とかなり長めとなっているところが多い印象です。数年単位になると、更新のタイミングでは当初構築や運用していた人がもういないという状況もありえるわけで、特定の人に依存せずに証明書更新を想定した運用ができるように作り込んでおかないと、ある日突然困ることになります。
証明書を更新しないとどうなる?
通常はSAMLを利用した認証連携(≒SSO)が動作しなくなります。サービス側ではSSOでしかログインする口を用意していない場合、サービスを利用できなくなります。というわけで、証明書更新作業を忘れてしまうとある日突然死にます。
証明書更新作業の流れ
ここからは、証明書の更新作業の流れについて書いています。
証明書更新時期に気づく
そもそも証明書更新時期が近づいていることに気づくのが第一歩ですね。
IdP側の運用が別の専門組織・部隊で行われている場合、(そちらがちゃんとしていれば)事前に証明書更新の連絡が各サービスの運用担当者に届くでしょう。それをトリガーに更新作業を行えばよさそうです。とはいえ、IdP側が連絡を忘れたり担当者が見逃す可能性もありますので、現在利用している証明書の有効期限をおさえて、自分たちでも有効期限切れの時期が近づいてきているのを検知できるような運用の仕組みを作っておくとよいでしょう。有効期限は、既に設定している証明書があれば、PEM形式のファイルにして openssl
コマンドで簡単に確認することができます[2]。
$ openssl x509 -noout -dates -in test-idp.cert
notBefore=Dec 9 01:05:26 2021 GMT
notAfter=Dec 9 01:05:08 2024 GMT
IdPもSPも同一組織で運用している場合は、自身で証明書更新の運用をしっかりと考えておかないと危険です。そもそもIdP側の証明書更新も忘れてしまう危険性もあります。
更新時期が近づいたらアラートをあげる仕組みを作っておくとか、カレンダーに登録しておいてメンバー全員に通知するなどでもいいかもしれません。
事前にIdP側の新しい証明書を発行
余裕をもって有効期限切れまでに、IdP側の新しい証明書を発行してもらう or します。まともなIdPなら、証明書の切り替えを考慮して複数の証明書を同時に設定できるようになっているはずです[3]。新しい証明書はまだ非アクティブな状態でIdP側の署名生成にはまだ使用されませんが、SAMLのメタデータとしては公開される状態になります。そして、サービス側はその新しい鍵を含むメタデータ(もしくは新しい証明書ファイル)を何らかの形で受け取ります。
新しい証明書への切り替え時期の調整
IdP側で新しい証明書に実際に切り替える(新しい方をアクティブにし、古い方を非アクティブにする)タイミングについては、IdPとSP担当者間で調整を行うケースがあるかもしれません。IdPによってはサービス単位で証明書を管理するケースがあります。この場合はサービス単位で個別に時期を調整可能になるでしょう。一方、全サービスで共通の証明書を利用している場合は、各サービスの事情に合わせてタイミングを調整することは難しくなるでしょう。
新しい証明書をSP (サービス) 側に設定
受け取った新しい証明書をサービスに設定しますが、ここでサービス側のSAML対応レベルによってはやり方が変わってきます。
Goodなサービスの場合
SP側でも新・旧の複数の証明書を設定できるようになっているとGoodです。切り替えタイミングに先立ってサービス側に新しい証明書を設定することができます。わざわざIdP側の切り替えタイミングに合わせて、せーのっで切り替える必要はありませんし、切り替えの間のログイン不可となる時間も0にできます。
残念なサービスの場合
残念ながらIdP側の証明書を1つしか設定できないタイプのサービスもあります。IdP側で切り替わる前に、先に設定してしまうと署名検証に失敗し障害になってしまいます。この場合は、IdP側の切り替えタイミングに合わせて頑張って更新しましょう。IdP側と完全にタイミングをあわせて切り替えることは難しいので、サービスにログイン不可の時間帯が発生する恐れがありますので、事前にエンドユーザへの告知も忘れずに。
IdP側で新しい証明書に切り替え実施
Goodなサービスの場合
事前に新しい証明書を設定済みなので、IdP側で切り替えてもらうのを待つだけです。IdP側で切り替え完了後に認証連携の問題が特に発生しないのであれば、古い証明書を削除して完了です。また、IdP側が何らかの理由で旧証明書にロールバックする、ということも想定して、証明書そのものの有効期限までは残しておく、という判断もあるかもしれません。この場合、IdP側でロールバックされてもSP側は何も変更しなくてよくなります。
残念なサービスの場合
極力ダウンタイムを短くするためには、IdP側の切り替えタイミングに合わせてSP側の証明書を更新します。IdPの運用が別組織の場合は、綿密にコミュニケーションをとりながら同時に切り替える必要があります。何か問題がありIdP側でロールバックされた場合は、旧証明書に戻す必要があります。
証明書更新の自動化
ここまで更新作業の流れを書きましたが、面倒くさい、そもそも更新作業が数年後となるとやっぱり忘れそう、といった感想を持つのが自然でしょう。そもそも証明書更新を手作業でやるのはやめて、自動化しておくという方式も考えられます。これを行うにはIdP側の対応も必要にはなります。
IdP側に必要な機能
まず、IdP側の証明書を自動的に取得できるように公開されている必要があります。対応しているIdPであれば、SMALメタデータのエンドポイントURLがあり、そこに対してGETリクエスを送るだけで証明書を含んだメタデータXMLを取得することができます。メジャーなIDaaSの一つであるAzure ADや、OSSのKeycloakではそのようなエンドポイントが提供されています。
SP側に必要な機能
SP側は、公開されているIdPのメタデータを自動的に取得できるような仕組みが必要になります。また、取得したメタデータ内の証明書を利用して、署名検証を行うような実装ももちろん必要です。
ネットワーク構成に注意
IdPまたはSPが社内ネットワーク上にある場合は注意が必要です。自動化を行うには、IdP => SPのアクセス経路が必要になります。SAMLのよくあるユースケースとしては、IdPとSP間の直接通信は不要なため、この間のアクセス経路が用意されていない可能性があります。特に、IdPが社内 / SPがSaaSなどインターネット上
はエンタープライズ用途ではよくあるパターンですが、IdPに対してインターネット経由でのアクセスは許可していない場合が多いでしょう。
参考: 複数の証明書のハンドリング
SP側で複数の証明書登録に対応させる場合、どのように実装するとよさそうか、参考までにOSSのKeycloakだとどういう実装になっているのか紹介します。
Keycloakの基本ユースケースはIdPとして使いますが、外部のIdPと認証連携するという使い方もできます。この場合、KeycloakはSPとして振る舞います。この外部IdPの設定では、複数のIdPの証明書を設定できるようになっています。
上図のように、管理コンソールから、カンマ区切りで複数の証明書(PEM形式)を入力することができます。
実際のロジックも見てみましょう。
そこまで長くないので、以下にコードとコメント(加筆)も記載します。
public static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
// 1) SAMLメタデータとSAMLレスポンスの双方に ds:KeyInfo/ds:KeyName があれば、該当の証明書の公開鍵を利用して署名を検証する
KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
try {
if (validateUsingKeySelector(signatureNode, sel)) {
return true;
}
if (sel.wasKeyLocated()) {
return false;
}
} catch (XMLSignatureException ex) { // pass through MarshalException
logger.debug("Verification failed for key " + sel.keyName + ": " + ex);
logger.trace(ex);
}
logger.trace("Could not validate signature using ds:KeyInfo/ds:KeyName hint.");
// 2) ds:KeyInfo/ds:KeyName で証明書を特定できない場合、SAMLメタデータで取得した証明書を順番に利用して署名検証を試みる
// 複数の証明書がある場合に対応する
if (locator instanceof Iterable) {
Iterable<Key> availableKeys = (Iterable<Key>) locator;
logger.trace("Trying hard to validate XML signature using all available keys.");
for (Key key : availableKeys) {
try {
if (validateUsingKeySelector(signatureNode, new KeySelectorPresetKey(key))) {
return true;
}
} catch (XMLSignatureException ex) { // pass through MarshalException
logger.debug("Verification failed: " + ex);
logger.trace(ex);
}
}
}
return false;
}
最初に、1) SAMLメタデータとSAMLレスポンスの双方に ds:KeyInfo/ds:KeyName
があれば、該当の証明書の公開鍵を利用して署名を検証します。これにより、複数の証明書が登録されていても該当の鍵を特定して効率的に検証が可能です(ds:KeyInfo/ds:KeyName
はOIDCでいうと、kid
と同じものですね)。
ただし、注意点としてIdP側が ds:KeyInfo/ds:KeyName
を送って来ない場合があります。例えばAzure ADは対応していないようです (メタデータにも含まれず、SAMLレスポンスにも存在しない)。
...
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC8DCCAdigAwIBAgIQL/rvnd...</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC8DCCAdigAwIBAgIQVM7spClF...</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
...
</IDPSSODescriptor>
...
一方、Keycloakの場合は対応しています。以下のように、ds:KeyInfo/ds:KeyName
が含まれています。
...
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:KeyName>_GkkpqHHSYv7gtd5596A3_ggT2o4ZzmL1a5iE3-Ww4Q</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>MIICnTCCAYUCBgF9ndbA9...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:KeyName>hiHPpSXmpaGEhrUoe8EyWTq_TJ59-PNYKtcl_PWjlzc</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>MIICnTCCAYUCBgF9nhBNW...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
...
</md:IDPSSODescriptor>
...
よって、様々なIdPに対応できるようにするには、ds:KeyInfo/ds:KeyName
に頼らない実装がSP側に必要となります。とはいってもやることは単純で、2)にて、SAMLレスポンス(アサーション)に含まれる署名を設定された証明書の公開鍵で順番に検証すればOKです。
まとめ
Digital Identity技術勉強会 #iddanceのカレンダー | Advent Calendar 2021 10日目の記事は、数年に1回忘れた頃にやってくるSAMLの証明書更新のお話でした。簡単にですが、証明書更新の必要性、更新を忘れるとどうなるか、切り替え手順の流れなどについて紹介しました。運用側の目線ですと、証明書更新の運用負荷を下げる仕組み (複数の証明書を事前に登録できたり、自動更新に対応していたり) があると大変うれしいですね! SAMLに対応した各サービスの作り手である方々には、こういうことも考慮して作り込んでもらえると非常に助かります。
-
https://docs.microsoft.com/ja-jp/azure/active-directory/manage-apps/manage-certificates-for-federated-single-sign-on#customize-the-expiration-date-for-your-federation-certificate-and-roll-it-over-to-a-new-certificate 参照 ↩︎
-
SAMLメタデータしかない場合は、証明書の文字列(
MIIC...
)をSAML DEVELOPER TOOLSなどでヘッダー付きのPEM形式に変換してopenssl
コマンドに渡せばOKです。 ↩︎ -
Azure ADやKeycloakではそのような作りになっています。 ↩︎
Discussion