Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ install: runtime
$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/serverless
$(ENV_INSTALL) apisix/plugins/serverless/*.lua $(ENV_INST_LUADIR)/apisix/plugins/serverless/

$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/signing
$(ENV_INSTALL) apisix/plugins/signing/*.lua $(ENV_INST_LUADIR)/apisix/plugins/signing/

$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/soap-signer
$(ENV_INSTALL) apisix/plugins/soap-signer/*.lua $(ENV_INST_LUADIR)/apisix/plugins/soap-signer/

$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/syslog
$(ENV_INSTALL) apisix/plugins/syslog/*.lua $(ENV_INST_LUADIR)/apisix/plugins/syslog/

Expand All @@ -357,6 +363,9 @@ install: runtime
$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/secret
$(ENV_INSTALL) apisix/secret/*.lua $(ENV_INST_LUADIR)/apisix/secret/

$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/xml-signer
$(ENV_INSTALL) apisix/plugins/xml-signer/*.lua $(ENV_INST_LUADIR)/apisix/plugins/xml-signer/

$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/zipkin
$(ENV_INSTALL) apisix/plugins/zipkin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/zipkin/

Expand Down
2 changes: 2 additions & 0 deletions apisix/cli/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ local _M = {
"ai-aws-content-moderation",
"ai-aliyun-content-moderation",
"ai-lakera-guard",
"soap-signer",
"xml-signer",
"proxy-mirror",
"graphql-proxy-cache",
"proxy-rewrite",
Expand Down
55 changes: 55 additions & 0 deletions apisix/plugins/signing/algorithms.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local digest = require("resty.openssl.digest")

local DS = "http://www.w3.org/2000/09/xmldsig#"

local ALGORITHMS = {
["rsa-sha1"] = {
digest = "sha1",
digest_uri = DS .. "sha1",
signature_uri = DS .. "rsa-sha1",
},
["rsa-sha256"] = {
digest = "sha256",
digest_uri = "http://www.w3.org/2001/04/xmlenc#sha256",
signature_uri = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
},
}

local _M = {}


function _M.get(name)
return ALGORITHMS[name]
end


function _M.hash(value, algorithm)
local context, err = digest.new(algorithm.digest)
if not context then
return nil, err
end
local ok, update_err = context:update(value)
if not ok then
return nil, update_err
end
return context:final()
end


return _M
103 changes: 103 additions & 0 deletions apisix/plugins/signing/credentials.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local pkey = require("resty.openssl.pkey")
local resty_sha256 = require("resty.sha256")
local resty_string = require("resty.string")
local x509 = require("resty.openssl.x509")

local tostring = tostring
local type = type


local credential_cache = core.lrucache.new({
ttl = 300,
count = 64,
})

local _M = {}


local function fingerprint(certificate_pem, private_key_pem)
local sha256 = resty_sha256:new()
sha256:update(certificate_pem)
sha256:update("\0")
sha256:update(private_key_pem)
return resty_string.to_hex(sha256:final())
end


local function parse_credentials(certificate_pem, private_key_pem)
local private_key, key_err = pkey.new(private_key_pem, {
format = "PEM",
type = "pr",
})
if not private_key then
return nil, "cannot parse RSA private key: " .. tostring(key_err)
end
local key_type = private_key:get_key_type()
if not key_type or key_type.sn ~= "rsaEncryption" then
return nil, "private key must be RSA"
end

local certificate, certificate_err = x509.new(certificate_pem, "PEM")
if not certificate then
return nil, "cannot parse X.509 certificate: " .. tostring(certificate_err)
end

local matches, match_err = certificate:check_private_key(private_key)
if not matches then
return nil, "private key does not match certificate: " .. tostring(match_err)
end

local certificate_der, der_err = certificate:tostring("DER")
if not certificate_der then
return nil, "cannot encode X.509 certificate: " .. tostring(der_err)
end

return {
private_key = private_key,
certificate = certificate,
certificate_der = certificate_der,
}
end


function _M.load(conf)
if type(conf) ~= "table" then
return nil, "credentials are required"
end

local certificate_pem = conf.certificate
local private_key_pem = conf.private_key
if type(certificate_pem) ~= "string" or certificate_pem == "" then
return nil, "certificate is required"
end
if type(private_key_pem) ~= "string" or private_key_pem == "" then
return nil, "private key is required"
end
if certificate_pem:sub(1, 1) == "$" or private_key_pem:sub(1, 1) == "$" then
return nil, "credential secret reference could not be resolved"
end

local cache_key = fingerprint(certificate_pem, private_key_pem)
return credential_cache(cache_key, nil, parse_credentials,
certificate_pem, private_key_pem)
end


return _M
151 changes: 151 additions & 0 deletions apisix/plugins/signing/request.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")

local ipairs = ipairs
local ngx = ngx
local tostring = tostring
local type = type

local _M = {}

local BODY_DIGEST_HEADERS = {
"Content-MD5",
"Digest",
"Content-Digest",
"Repr-Digest",
}


local function contains(values, expected)
for _, value in ipairs(values) do
if value == expected then
return true
end
end
return false
end


local function normalized_content_type(value)
if not value then
return ""
end
local normalized = value:match("^%s*([^;]+)")
return normalized and normalized:lower() or ""
end


local function has_unsupported_charset(value)
if not value then
return false
end

local parameter_start
local quoted = false
local escaped = false

for index = 1, #value + 1 do
local char = value:sub(index, index)
if index > #value or (char == ";" and not quoted) then
if parameter_start then
local parameter = value:sub(parameter_start, index - 1)
local name, charset = parameter:match(
"^%s*([^=%s]+)%s*=%s*(.-)%s*$")
if name and name:lower() == "charset" then
charset = charset:match("^%s*(.-)%s*$")
if charset:sub(1, 1) == '"' and charset:sub(-1) == '"' then
charset = charset:sub(2, -2)
end
charset = charset:lower()
if charset ~= "utf-8" and charset ~= "utf8" then
return true
end
end
end
parameter_start = index + 1
elseif char == '"' and not escaped then
quoted = not quoted
end

if char == "\\" and quoted and not escaped then
escaped = true
else
escaped = false
end
end
return false
end


function _M.rewrite(plugin_name, conf, ctx, sign)
if type(conf) ~= "table" then
return 500, {message = "internal_error"}
end

conf.request = conf.request or {}
conf.request.methods = conf.request.methods or {"POST"}
conf.request.content_types = conf.request.content_types or {"application/xml"}
conf.request.max_body_bytes = conf.request.max_body_bytes or 5242880

if not contains(conf.request.methods, ngx.req.get_method()) then
return
end

local content_type_header = core.request.header(ctx, "Content-Type")
local content_type = normalized_content_type(content_type_header)
if not contains(conf.request.content_types, content_type) then
return 415, {message = "unsupported_media_type"}
end
if has_unsupported_charset(content_type_header) then
return 415, {message = "unsupported_media_type"}
end

local body, body_err = core.request.get_body(conf.request.max_body_bytes, ctx)
if not body then
core.log.error(plugin_name, " failed to read request body: ", body_err)
if not body_err then
return 400, {message = "empty_body"}
end
if body_err:find("is greater than the maximum size", 1, true) then
return 413, {message = "body_too_large"}
end
return 500, {message = "internal_error"}
end

if conf.signature.algorithm == "rsa-sha1" then
core.log.warn(plugin_name, " is using legacy SHA-1 cryptography")
end
Comment thread
keremoge marked this conversation as resolved.

local signed, code = sign(body, conf)
if not signed then
core.log.error(plugin_name, " rejected request: ", code)
local internal_error = code == "credential_error" or code == "signing_error"
local status = internal_error and 500 or 400
return status, {message = code or "internal_error"}
end

ngx.req.set_body_data(signed)
core.request.set_header(ctx, "Content-Length", tostring(#signed))
ngx.req.clear_header("Transfer-Encoding")
for _, header in ipairs(BODY_DIGEST_HEADERS) do
ngx.req.clear_header(header)
end
end


return _M
Loading
Loading