如何验证 HTTP 重定向绑定的 SAML 签名

How to verify a SAML signature for HTTP-redirect binding

我正在通过 HTTP 重定向绑定接收 SAML 请求,SAML 请求的内容如下所示

{"SigAlg"=>"http://www.w3.org/2000/09/xmldsig#rsa-sha1", "SAMLRequest"=>"lVLLaoQwFP0VyT5jEqPG4AiFoSDMtNApXXQzxDxaQRObRDqfX3XoolAKXd7DPQ/uuXUQ4zDxo3tzc3zSH7MOMWkPe3DpcixzVVVQl4RBqoiCncEYEmkoY7k00hCQvGgfemf3gOwQSNoQZt3aEIWNC4RwCRGGiD6jkmPMs2KHUPYKksPi0lsRN+Z7jFPgafqpvejtbtQpSK7jYAPfsu3B7C13IvSBWzHqwKPk57vTkS+WfPIuOukG0NSbub9R/yaJELRfzUGzrhmtFut15qdeeheciY926K2u05toUz8sIu0huXd+FPFv9RXpFTTbKp/WA4WobQT/jEYrykwhNaQ66yDNMwY7wijEtMCmysqqo6xOb8Ga+tbjWYe1jtYqfW0uCucoYwWCHS3F0kRGoajWTpAiiJRZJRmu01+Y3+CPt2i+AA=="}

它还有一个签名值

WkDaGzC6vPTlzh+EnFA5/8IMmV7LviyRh2DA5EHF0K0nl+xzBlKfNCYRnunpwoEvGhereGdI5xBpv+mc9IguiCaLZSZjDh6lIDdpvctCnmSNzORqzWQwQGeZ9vjgtCLjUn35VZLNs3WgEqbi2cL+ObrUDS2gV1XvBA3Q3RRhoDmi+XE89Ztnd1cNpR3XdA+EL2ENbMI2XAD9qSgMufUJY/3GBBpT7Vg1ODtPxBudq+sXrgPh/+WtUUitLkkfC8tdRTCS1EZPv+h27I5g/VNza23Xl8w2HdAuYP0F2FjREo8VV2aUtaOUd/jAF9+bfkGV93y1PzFttLxdBbFoxp6qBg==

但是我不明白如何验证这个签名是否正确。

关于 SAML 绑定的第 3.4.4.1 节 https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf

To construct the signature, a string consisting of the concatenation of the RelayState (if present),
SigAlg, and SAMLRequest (or SAMLResponse) query string parameters (each one URLencoded)
is constructed in one of the following ways (ordered as below):
SAMLRequest=value&RelayState=value&SigAlg=value
SAMLResponse=value&RelayState=value&SigAlg=value

我尝试了这种方法,但是

<samlp:LogoutRequest ID="_36167d94-d868-4c04-aee3-8bbd4ed91317" Version="2.0" IssueInstant="2017-01-05T16:21:55.704Z" Destination="https://werain.me/" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">urn:federation:MicrosoftOnline</Issuer><NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">4948f6ce-4e3b-4538-b284-1461f9379b48</NameID><samlp:SessionIndex>_eafbb730-b590-0134-a918-00d202739c81</samlp:SessionIndex></samlp:LogoutRequest>

这里有任何帮助。

SAML 身份验证消息是一个 XML 具有嵌入(封装)XMLDSig 签名或压缩编码签名的文档

信封XMLDSign签名

<samlp:LogoutRequest>
    <...saml message...> 
    <ds:Signature>
         <ds:SignedInfo />
         <ds:SignatureValue /> 
         <ds:KeyInfo /> 
    </ds:Signature> 
</samlp:LogoutRequest>

<ds:SignatureValue> 包含签名、<ds:SignedInfo> 签名数据和对消息的引用,<ds:KeyInfo> 通常包含带有签名者身份的 X509Certificate 或对该消息的引用证书

URL

中的压缩编码
SAMLRequest=value&RelayState=value&SigAlg=value&Signature=value

每个值都经过 url 编码

SAMLRequest=urlencode(base64(<samlp:LogoutRequest> <...saml message...> </samlp:LogoutRequest>))

并且签名是使用算法 SigAlg

在查询字符串算法的串联上完成的
Signature = urlencode( base64 ( SigAlg ("SAMLRequest=value&RelayState=value&SigAlg=value")))

SAML 消息的数字签名

SAML 消息使用发行者 (SP) 的私钥进行数字签名(未加密),并且可以使用 SP 的 public 密钥进行验证. SAML 响应必须使用身份提供者 (IdP) 的私钥进行签名,SP 可以使用 IdP 的 public 密钥验证消息。

如果您作为 IdP 并且想要验证 SP 的 SAML 请求,您需要:

  • 验证数字签名:使用SP的public密钥验证签名与签名消息是否匹配,以确保身份签名者和消息未被更改

  • 授权请求:验证签名者的身份是否可以执行请求的操作。通常您必须将证书的序列号或主题与预先存在的列表进行匹配,或者验证证书是否由受信任的证书颁发机构颁发

  • 生成 SAML 响应:使用 SAML 数据生成 XML 消息并使用您的私钥对其进行签名以发送至 SP

大多数编程语言都支持 XMLDsig 签名,但在您的情况下使用的是 deflated encoding,这是 SAML 绑定的特定特征,因此如果您的 SAML 库不支持,您必须手动验证签名。这些或多或少是根据 specification

遵循的步骤
 //get params from query string 
String samlrequest = getQueryParam("SAMLRequest");
String relaystate = getQueryParam("RelayState");
String sigalg = getQueryParam("SigAlg");
String signature = getQueryParam("Signature");


//The signature
byte signature[] = URLDecoder.decode(Base64.getDecoder().decode(signature ), "UTF-8");

//The signed data. build the following string checking if RelayState is null
//SAMLRequest=samlrequest&RelayState=relaystate&SigAlg=sigalg
byte signedData[] = concat(samlrequest,relaystate,sigalg);

//The signature algorithm could be "SHA1WithRSA" or "SHA1withDSA" depending on sigalg is http://www.w3.org/2000/09/xmldsig#rsa-sha1 or http://www.w3.org/2000/09/xmldsig#dsa-sha1 
String signatureAlgorithm = extractSignatureAlgorithm(sigalg);

//get the public key of the SP. It must be registered before this process
PublicKey publicKey = ...

//Verify the signature
Signature sig = Signature.getInstance(signatureAlgorithm);
sig.initVerify(publicKey);
sig.update(signedData); 
boolean verifies = sig.verify(signature);  

我正在尝试使用上述答案,但没有成功。

然后,阅读文档并花一点时间,我成功地验证了 Java 的签名,快速的答案是:

final String samlRequest = request.getParameter("SAMLRequest");
final String relayState = request.getParameter("RelayState");
final String sigAlg = request.getParameter("SigAlg");
final String signature = request.getParameter("Signature");

FileInputStream fis = new FileInputStream(new File("path-to-service-provider-x509-certificate"));

CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(fis);

// ps: java.net.URLEncoder;
String query = "SAMLRequest=" + URLEncoder.encode(samlRequest, "UTF-8");
query += "&RelayState=" +URLEncoder.encode(relayState, "UTF-8");
query += "&SigAlg=" + URLEncoder.encode(sigAlg, "UTF-8");

// ps: org.opensaml.xml.util.Base64
byte[] signatureBytes = Base64.decode(signature);

org.apache.xml.security.Init.init();
Signature sig = Signature.getInstance("SHA1withRSA"); // or other alg (i, e: SHA256WithRSA or others)
sig.initVerify(cert.getPublicKey());
sig.update(query.getBytes());
Boolean valid = sig.verify(signatureBytes);

我想在上面的答案中补充一点:URL encoding/decoding 是非规范的,意味着每个 framework/language 可能 其实有不同的做法。很多天我一直在验证 HTTP-Redirect 绑定,结果发现我们使用的 Java Play 1.x 框架 URL 以不同于 SAML 框架预期的方式解码事物。

我们通过直接从查询字符串中提取查询参数来解决这个问题,而不是让 Play 框架为我们解码它(只是为了我们需要重新编码)。因此,如果您的代码与 Alexandre 匹配,但 SAML 框架表示签名无效,请确保您将 直接 从 URL GET 中获取的字符串输入算法参数。

SAML 2.0 签名的验证方式因绑定(POST 或重定向)而异。如果使用 POST 绑定,则在 SAML XML 中验证签名。如果使用重定向绑定,则使用签名验证查询字符串。

此 LogoutRequest 是通过重定向绑定发送的。 以下 C# 示例代码是从 ITfoxtec.Identity.Saml2 组件复制而来的,并展示了如何验证签名。

var queryString = request.QueryString;
var signatureValue = Convert.FromBase64String(request.Query["Signature"]);

var messageName = "SAMLRequest";
var signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
var signatureValidationCertificate = new X509Certificate2("path-to-service-provider-x509-certificate");

var saml2Sign = new Saml2SignedText(signatureValidationCertificate, signatureAlgorithm);
if (saml2Sign.CheckSignature(Encoding.UTF8.GetBytes(new RawSaml2QueryString(queryString, messageName).SignedQueryString), signatureValue))
{
    // Signature is valid.
}
else
{
    throw new InvalidSignatureException("Signature is invalid.");
}

对于那些仍然卡住的人,这是完整的方法

public static void verifySignature(boolean isResponse, String samlQueryString, String relayStateString, String sigAlgString, String signature, X509Certificate cert) throws Exception {
    String type = isResponse ? "SAMLResponse" : "SAMLRequest";

    String query = type + "=" + URLEncoder.encode(samlQueryString, "UTF-8");
        query += relayStateString == null ? "" : "&RelayState=" + URLEncoder.encode(relayStateString, "UTF-8");
        query += "&SigAlg=" + URLEncoder.encode(sigAlgString, "UTF-8");

    String javaSigAlgName = null;

    if(sigAlgString.equals("http://www.w3.org/2000/09/xmldsig#rsa-sha1")) {
        javaSigAlgName = "SHA1withRSA";
    } else if(sigAlgString.equals("http://www.w3.org/2000/09/xmldsig#rsa-sha256")) {
        javaSigAlgName = "SHA256withRSA";
    } else {
        throw new Exception("signature: " + sigAlgString + " not supported by SP/IDP");
    }

    byte[] signatureBytes = Base64.getDecoder().decode(signature);

    Signature sig = Signature.getInstance(javaSigAlgName);
    sig.initVerify(cert.getPublicKey());
    sig.update(query.getBytes());

    Boolean valid = sig.verify(signatureBytes);
    System.out.println("is valid: " + valid);
}

我们可以使用 one login saml library 来验证 auth-request signature.They 提供了很多包装器方法 SAML.This 是它的 ruby 实现。 `

def verify_signature(params)
    saml_request = URI.decode(params[:SAMLRequest])
    relay_state_string = URI.decode(params[:RelayState])
    signature = URI.decode(params[:Signature])
    sign_alg = URI.decode(params[:SigAlg])
    query_params,sig_params={},{}
    query_params[:type] = "SAMLRequest"
    query_params[:data] = saml_request
    query_params[:relay_state] = relay_state_string
    query_params[:sig_alg] = sign_alg
    query = OneLogin::RubySaml::Utils.build_query(query_params)
    sig_params[:cert] = getPublicKeyFromCertificate
    sig_params[:sig_alg] = sign_alg
    sig_params[:signature] = signature
    sig_params[:query_string] = query
    OneLogin::RubySaml::Utils.verify_signature(sig_params)
end

`