如何在服务端Swift HTTPS端点正确验证SNS消息?
解决Amazon SNS消息签名验证失败问题(Swift/Vapor环境)
问题根源分析
你的签名验证始终失败,核心原因有三点:
- 待签名字符串格式不符合要求:亚马逊SNS要求
stringToSign严格遵循键值对顺序,每个键和对应值各占一行,末尾不能有多余空行。你的代码中最后多了两个换行,导致生成的哈希值与亚马逊原始哈希不匹配。 - 缺少签名证书合法性验证:直接使用获取到的证书存在安全风险,且未验证证书有效期、签发者,虽然这不是签名失败的直接原因,但必须补上以避免伪造证书攻击。
- Swift Crypto签名验证调用方式错误:亚马逊SNS使用RSA-PKCS#1 v1.5签名方案,你直接传入预计算的Digest对象,未明确指定对应哈希算法方案,导致无法匹配亚马逊的签名格式。
修正后的代码实现
1. 修正待签名字符串格式
extension SubscriptionConfirmationMessage: VerifiableMessage { var stringToSign: String { """ Message \(message) MessageId \(messageID) SubscribeURL \(subscribeURL) Timestamp \(timestamp) Token \(token) TopicArn \(topicARN) Type SubscriptionConfirmation """ } }
2. 完善签名验证逻辑
func messageHasValidSignature<Message: VerifiableMessage>(_ message: Message) async throws -> Bool { // 验证签名证书URL的域名合法性(必须是亚马逊官方域名) guard let url = URL(string: message.signingCertificateURL), let host = url.host, (host.hasSuffix(".amazonaws.com") || host.hasSuffix(".amazonaws.cn")) else { logger.error("非法的签名证书URL域名") throw Abort(.badRequest) } // 获取证书内容 guard let certificateBytes = try await client.get(.init(message.signingCertificateURL)).body else { logger.error("获取签名证书内容失败") throw Abort(.badRequest) } let certificateString = String(buffer: certificateBytes) let certificate = try Certificate(pemEncoded: certificateString) // 验证证书有效期 guard Date() >= certificate.validity.notBefore, Date() <= certificate.validity.notAfter else { logger.error("签名证书已过期或未到生效时间") throw Abort(.badRequest) } // 验证证书签发者为Amazon SNS(生产环境建议配置根CA做完整链验证) guard certificate.subject.commonName?.contains("Amazon SNS") == true else { logger.error("证书非Amazon SNS签发") throw Abort(.badRequest) } // 提取公钥 let publicKey = try _RSA.Signing.PublicKey(derRepresentation: certificate.publicKey.subjectPublicKeyInfoBytes) // 解码签名数据 guard let signatureData = Data(base64Encoded: message.signature) else { logger.error("签名数据Base64解码失败") throw Abort(.badRequest) } let signature = _RSA.Signing.RSASignature(rawRepresentation: signatureData) // 转换待签名字符串为UTF-8数据 guard let dataToSign = message.stringToSign.data(using: .utf8) else { logger.error("待签名字符串转UTF-8数据失败") throw Abort(.badRequest) } // 根据签名版本选择对应的签名方案 let scheme: _RSA.Signing.RSASigningScheme switch message.signatureVersion { case ._1: scheme = .insecureSHA1 case ._2: scheme = .sha256 } do { return try publicKey.isValidSignature(signature, for: dataToSign, using: scheme) } catch { logger.error("签名验证失败: \(error.localizedDescription)") return false } }
额外注意事项
- 对于
Notification、UnsubscribeConfirmation等其他类型的SNS消息,需按照亚马逊文档对应的字段顺序构造stringToSign,不能混用字段。 - 生产环境建议配置亚马逊根CA证书,做完整的证书链验证,避免仅检查主题名称的局限性。
内容的提问来源于stack exchange,提问作者Ryan Burgoyne
相关产品推荐
相关产品推荐

