OpenAMにSAMLアサーションを複数送ってみた
2021-06-14 - 相本 智仁
はじめに
Lasso の脆弱性はSAMLアサーションが複数存在した時の署名検証の不備でした。 OpenAM は Lasso を使っていませんので Lasso の脆弱性は当てはまりませんが、 SAML の SP として動作することが出来ます。 OpenAM の実装としてSAMLアサーションが複数送られてきた場合どのような挙動(仕様)なのかを調べてみました。
結論
OpenAMには Lasso の脆弱性(CVE-2021-28901)にあたる問題はありませんでした。
- OpenAM はSAMLアサーションが複数送られてくることを許容する
- 複数送られてきたSAMLアサーションの全ての署名を検証する
- 一つでも「署名されていない」「署名の検証に失敗」すればエラーとする
- 複数のSAMLアサーションが存在しても、使用するアサーションは一つ
- 最初に有効と判定したアサーションのみを使ってアカウントのマッピングを行う
検証内容
以下の内容の検証を実施しました。
- OpenAM を SAML SP として構築(SAML2認証モジュールを使用)
- SAMLアサーションが2つ存在するSAMLレスポンスを受けった場合のOpenAMの挙動を確認する
- パターン1 -> 署名あり、署名なしの順
- パターン2 -> 署名なし、署名ありの順
- パターン3 -> 署名あり、署名ありの順
パターン1,パターン2のケース
どちらも Federation ログに以下のエラーが出力されて認証失敗となりました。
libSAML2:06/10/2021 07:27:40:748 AM UTC: Thread[https-jsse-nio-8443-exec-9,5,main]: TransactionId[ffdf7b49-66f5-4463-ac45-32022fb927b5-1587]
ERROR: SAML2Utils.verifyResponse:WantAssertionsSigned is true but some or all assertions are not signed
パターン3
OpenAMでエラーとならず、認証に成功しました。
ソースコードで確認する
検証で実際の動作は確認出来たので、ソースコードでも確認してみます。 コードは該当箇所のみ抜粋します。
全部を見たい方はOpenAMコンソーシアムのソースコードで確認可能です。
280 public static Map verifyResponse(
281 final HttpServletRequest httpRequest,
282 final HttpServletResponse httpResponse,
283 final Response response,
284 final String orgName,
285 final String hostEntityId,
286 final String profileBinding)
287 throws SAML2Exception {
〜
499 Map smap = null;
〜
504 for (Assertion assertion : assertions) {
505 String assertionID = assertion.getID();
〜
535 if (assertion.isSigned()) {
536 if (verificationCerts == null) {
537 idp = saml2MetaManager.getIDPSSODescriptor(
538 orgName, idpEntityId);
539 verificationCerts = KeyUtil.getVerificationCerts(idp, idpEntityId, SAML2Constants.IDP_ROLE);
540 }
541 if (CollectionUtils.isEmpty(verificationCerts) || !assertion.isSignatureValid(verificationCerts)) {
542 debug.error(method +
543 "Assertion is not signed or signature is not valid.");
544 String[] data = {assertionID};
545 LogUtil.error(Level.INFO,
546 LogUtil.INVALID_SIGNATURE_ASSERTION,
547 data,
548 null);
549 throw new SAML2Exception(bundle.getString(
550 "invalidSignatureOnAssertion"));
551 }
552 } else {
553 allAssertionsSigned = false;
554 }
555 List authnStmts = assertion.getAuthnStatements();
556 if (authnStmts != null && !authnStmts.isEmpty()) {
〜
600 if (smap == null) {
601 smap = fillMap(authnStmts,
602 subject,
603 assertion,
604 assertions,
605 reqInfo,
606 inRespToResp,
607 orgName,
608 hostEntityId,
609 idpEntityId,
610 spConfig,
611 (Date) bearerMap.get(SAML2Constants.NOTONORAFTER));
612 }
613 } // end of having authnStmt
614 }
〜
644 if (!responseIsSigned && !allAssertionsSigned) {
645 debug.error(method + "WantAssertionsSigned is true but some or all assertions are not signed");
646 String[] data = { orgName, hostEntityId, idpEntityId };
647 LogUtil.error(Level.INFO, LogUtil.INVALID_SIGNATURE_ASSERTION, data, null);
648 throw new SAML2Exception(bundle.getString("assertionNotSigned"));
649 }
650 }
651
652 return smap;
653 }
受け取ったSAMLレスポンスを検証するverifyResponseメソッドのコードです。
504行目のfor文が送られてきたアサーションの数だけ繰り返されます。複数アサーションが考慮されていることが見て取れます。
535行目でアサーションが署名されていれば真となります。
真の場合、IdPのメタデータからサーバーの証明書を取り出して署名検証を行います。
署名検証に失敗した場合は SAML2Exceptionを throw します。
偽の場合(アサーションが署名されていない場合)、 553行目で allAssertionsSigned
に false
がセットされます。
この一連の処理はfor文の中ですので一つでもアサーションに署名されていなければ
allAssertionsSigned
に false
がセットされます。
504行目のfor文を抜けたあとの644行目を見るとallAssertionsSigned
がfalse
なら SAML2Exception が throw されることが分かります。
パターン1、パターン2の検証は644行目のif文が真となりエラーがthrowされるコードを通っています。
644行目の responseIsSigned
は、SAMLレスポンスのXMLが署名されていて検証に成功すれば true
がセットされています。
XML自体の署名の確認が取れていれば、SAMLレスポンスは改ざんはされていないことが保証されますので個々のアサーションでは
署名が無くても OK ということになっています。
終わりに
ソースコードは署名検証にフォーカスして説明しました。 2つの署名が有効なSAMLアサーションが送られてきた場合は、 verifyResponseメソッドとこのメソッドを呼び出しているコードを見れば確認出来ますので興味がある方はぜひご自分で確認してみてください。
ソフトウェアがどういう挙動(仕様)なのかを把握するのに、「実際に検証してみる」と「ソースコード」の両面から確認することで深く理解することが出来ました。 今後も疑問に思ったことは、実際に検証し、ソースコードで裏を取るというのを心がけて行きたいと思います。