lua-resty-acme

自动 Let's Encrypt 证书服务和 ACME 协议的 Lua 实现

$ opm get thenam153/lua-resty-acme

lua-resty-acme

自动 Let's Encrypt 证书服务 (RSA + ECC) 和 ACMEv2 协议的纯 Lua 实现。

http-01tls-alpn-01 挑战都得到支持。

!构建状态 !luarocks !opm

简体中文

描述

该库包含两部分

  • resty.acme.autossl: Let's Encrypt 证书的自动生命周期管理

  • resty.acme.client: ACME v2 协议的 Lua 实现

使用 opm 安装

    opm install thenam153/lua-resty-acme

或者,使用 luarocks 安装

    luarocks install lua-resty-acme
    # manually install a luafilesystem
    luarocks install luafilesystem

注意,使用 LuaRocks 时,您需要手动安装 luafilesystem。 这样做是为了保持向后兼容性。

该库使用 基于 FFI 的 openssl 后端,目前支持 OpenSSL 1.1.11.1.01.0.2 系列。

状态

生产。

概要

创建帐户私钥和备用证书

    # create account key
    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /etc/openresty/account.key
    # create fallback cert and key
    openssl req -newkey rsa:2048 -nodes -keyout /etc/openresty/default.key -x509 -days 365 -out /etc/openresty/default.pem

使用以下示例配置

    events {}
    
    http {
        resolver 8.8.8.8 ipv6=off;
    
        lua_shared_dict acme 16m;
    
        # required to verify Let's Encrypt API
        lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
        lua_ssl_verify_depth 2;
    
        init_by_lua_block {
            require("resty.acme.autossl").init({
                -- setting the following to true
                -- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
                tos_accepted = true,
                -- uncomment following for first time setup
                -- staging = true,
                -- uncomment following to enable RSA + ECC double cert
                -- domain_key_types = { 'rsa', 'ecc' },
                -- uncomment following to enable tls-alpn-01 challenge
                -- enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
                account_key_path = "/etc/openresty/account.key",
                account_email = "youemail@youdomain.com",
                domain_whitelist = { "example.com" },
            })
        }
    
        init_worker_by_lua_block {
            require("resty.acme.autossl").init_worker()
        }
    
        server {
            listen 80;
            listen 443 ssl;
            server_name example.com;
    
            # fallback certs, make sure to create them before hand
            ssl_certificate /etc/openresty/default.pem;
            ssl_certificate_key /etc/openresty/default.key;
    
            ssl_certificate_by_lua_block {
                require("resty.acme.autossl").ssl_certificate()
            }
    
            location /.well-known {
                content_by_lua_block {
                    require("resty.acme.autossl").serve_http_challenge()
                }
            }
        }
    }

在测试部署时,建议取消注释 staging = true 以允许对您的环境进行端到端测试。 这可以避免配置失败导致过多请求,从而导致 Let's Encrypt API 的 速率限制

默认情况下,autossl 仅创建 RSA 证书。 要使用 ECC 证书或两者,请取消注释 domain_key_types = { 'rsa', 'ecc' }。 请注意,多个证书链仅受 NGINX 1.11.0 或更高版本支持。

证书将在 Nginx 看到具有此类 SNI 的请求后被排队 创建,这可能需要数十秒才能完成。在此期间,具有此类 SNI 的请求将使用备用证书进行响应。

请注意,必须设置 domain_whitelistdomain_whitelist_callback 以包含您希望为其提供 autossl 服务的域,以防止使用 SSL 握手中的虚假 SNI 造成潜在的滥用。

domain_whitelist 定义一个表,其中包含所有应包含的域以及用于创建证书的 CN。 仅允许单个 * 作为通配符。

    domain_whitelist = { "domain1.com", "domain2.com", "domain3.com", "*.domain4.com" },

通配符证书

要使该库能够创建通配符证书,必须满足以下要求

  • 通配符域应在 domain_whitelist 中完全显示为 *.somedomain.com

  • 已启用 dns-01 挑战,并且已配置一个 DNS 提供商,该提供商具有与域匹配的 domains

否则,将创建非通配符证书作为备用。

默认情况下,通配符域 *.example.com 将出现在公用名中。 但是,当 wildcard_domain_in_san 设置为 true 时,将创建具有公用名 example.com 和主题备用名称 *.example.com 的证书。 请注意,*.example.comexample.com 应该出现在 dns_provider_accounts 中。

高级用法

使用函数来包含域

domain_whitelist_callback 定义一个函数,该函数接受域作为参数,并返回布尔值以指示是否应包含该域。

要匹配域名的模式,例如 example.com 下的所有子域,请使用

    domain_whitelist_callback = function(domain, is_new_cert_needed)
        return ngx.re.match(domain, [[\.example\.com$]], "jo")
    end

此外,由于在证书阶段运行域名白名单检查。 这里可以使用 cosocket API。 请注意,这将增加 SSL 握手延迟。

    domain_whitelist_callback = function(domain, is_new_cert_needed)
        -- send HTTP request
        local http = require("resty.http")
        local res, err = httpc:request_uri("http://example.com")
        -- access the storage
        local acme = require("resty.acme.autossl")
        local value, err = acme.storage:get("key")
        -- get cert from resty LRU cache
        -- cached = { pkey, cert } or nil if cert is not in cache
        local cached, staled, flags = acme.get_cert_from_cache(domain, "rsa")
        -- do something to check the domain
        -- return is_domain_included
    end}),

domain_whitelist_callback 函数提供第二个参数,指示证书将要用于传入 HTTP 请求(false)还是将要请求新证书(true)。 这允许在热路径(服务请求)上使用缓存的值,同时为新证书从存储中获取最新数据。 人们还可以实现不同的逻辑,例如在请求新证书之前进行额外的检查。

定义失败冷静期

如果证书请求失败,您可能希望阻止 ACME 客户端立即请求另一个证书。 默认情况下,冷静期设置为 300 秒(5 分钟)。 它可以通过 failure_coolofffailure_cooloff_callback 函数自定义,例如实现指数回退。

        failure_cooloff_callback = function(domain, count)
          if count == 1 then
            return 600 -- 10 minutes
          elseif count == 2 then
            return 1800 -- 30 minutes
          elseif count == 3 then
            return 3600 -- 1 hour
          elseif count == 4 then
            return 43200 -- 12 hours
          elseif count == 5 then
            return 43200 -- 12 hours
          else
            return 86400 -- 24 hours
          end
        end

tls-alpn-01 挑战

tls-alpn-01 挑战目前在 Openresty 1.15.8.x1.17.8.x1.19.3.x 上得到支持。

<details> <summary>点击展开示例配置</summary>

    events {}
    
    http {
        resolver 8.8.8.8 ipv6=off;
    
        lua_shared_dict acme 16m;
    
        # required to verify Let's Encrypt API
        lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
        lua_ssl_verify_depth 2;
    
        init_by_lua_block {
            require("resty.acme.autossl").init({
                -- setting the following to true
                -- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
                tos_accepted = true,
                -- uncomment following for first time setup
                -- staging = true,
                -- uncomment folloing to enable RSA + ECC double cert
                -- domain_key_types = { 'rsa', 'ecc' },
                -- uncomment following to enable tls-alpn-01 challenge
                enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
                account_key_path = "/etc/openresty/account.key",
                account_email = "youemail@youdomain.com",
                domain_whitelist = { "example.com" },
                storage_adapter = "file",
            })
        }
        init_worker_by_lua_block {
            require("resty.acme.autossl").init_worker()
        }
    
        server {
            listen 80;
            listen unix:/tmp/nginx-default.sock ssl;
            # listen unix:/tmp/nginx-default.sock ssl proxy_protocol;
            server_name example.com;
    
            # set_real_ip_from unix:;
            # real_ip_header proxy_protocol;
    
            # fallback certs, make sure to create them before hand
            ssl_certificate /etc/openresty/default.pem;
            ssl_certificate_key /etc/openresty/default.key;
    
            ssl_certificate_by_lua_block {
                require("resty.acme.autossl").ssl_certificate()
            }
    
            location /.well-known {
                content_by_lua_block {
                    require("resty.acme.autossl").serve_http_challenge()
                }
            }
        }
    }
    
    stream {
        init_worker_by_lua_block {
            require("resty.acme.autossl").init({
                -- setting the following to true
                -- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
                tos_accepted = true,
                -- uncomment following for first time setup
                -- staging = true,
                -- uncomment folloing to enable RSA + ECC double cert
                -- domain_key_types = { 'rsa', 'ecc' },
                -- uncomment following to enable tls-alpn-01 challenge
                enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
                account_key_path = "/etc/openresty/account.key",
                account_email = "youemail@youdomain.com",
                domain_whitelist = { "example.com" },
                storage_adapter = "file"
            })
            require("resty.acme.autossl").init_worker()
        }
    
        map $ssl_preread_alpn_protocols $backend {
            ~\bacme-tls/1\b unix:/tmp/nginx-tls-alpn.sock;
            default unix:/tmp/nginx-default.sock;
        }
    
        server {
                listen 443;
                listen [::]:443;
    
                ssl_preread on;
                proxy_pass $backend;
    
                # proxy_protocol on;
        }
    
        server {
                listen unix:/tmp/nginx-tls-alpn.sock ssl;
                # listen nix:/tmp/nginx-tls-alpn.sock ssl proxy_protocol;
                ssl_certificate certs/default.pem;
                ssl_certificate_key certs/default.key;
    
                # requires --with-stream_realip_module
                # set_real_ip_from unix:;
    
                ssl_certificate_by_lua_block {
                        require("resty.acme.autossl").serve_tls_alpn_challenge()
                }
    
                content_by_lua_block {
                        ngx.exit(0)
                }
        }
    }

</details>

在上面的示例配置中,我们设置了一个 http 服务器和两个流服务器。

最前面的流服务器监听 443 端口,并根据客户端 ALPN 路由到不同的上游。 tls-alpn-01 响应器监听 unix:/tmp/nginx-tls-alpn.sock。 所有正常的 https 流量监听 unix:/tmp/nginx-default.sock

                                                    [stream server unix:/tmp/nginx-tls-alpn.sock ssl]
                                                Y /
    [stream server 443] --- ALPN is acme-tls ?
                                                N \
                                                    [http server unix:/tmp/nginx-default.sock ssl]
  • 传递给 require("resty.acme.autossl").init 的配置在两个子系统中应尽可能保持一致。

  • tls-alpn-01 挑战处理程序不需要任何第三方依赖项。

  • 您可以同时启用 http-01tls-alpn-01 挑战处理程序。

  • httpstream 子系统不共享 shm,因此考虑使用除 shm 之外的存储。 如果您必须使用 shm,则需要应用 此补丁

dns-01 挑战

DNS-01 挑战在 lua-resty-acme > 0.13.0 上得到支持。 当前,支持以下 DNS 提供商

  • cloudflare: Cloudflare

  • dynv6: Dynv6

  • dnspod-intl: Dnspod 国际版 (仅支持 Dnspod 令牌,并在 secret 字段中使用 id,token)

要了解如何扩展新的 DNS 提供商以与 dns-01 挑战配合使用,请参见 "DNS 提供商"

使用 dns-01 挑战的示例配置将是

    require("resty.acme.autossl").init({
      -- setting the following to true
      -- implies that you read and accepted https://letsencrypt.openssl.ac.cn/repository/
      tos_accepted = true,
      -- uncomment following for first time setup
      -- staging = true,
      -- uncomment following to enable RSA + ECC double cert
      -- domain_key_types = { 'rsa', 'ecc' },
      -- do not set `http-01` or `tls-alpn-01` if you only plan to use dns-01.
      enabled_challenge_handlers = { 'dns-01' },
      account_key_path = "/etc/openresty/account.key",
      account_email = "youemail@youdomain.com",
      domain_whitelist = { "example.com", "subdomain.anotherdomain.com" },
    
      dns_provider_accounts = {
        {
          name = "cloudflare_prod",
          provider = "cloudflare",
          secret = "apikey of cloudflare",
          domains = { "example.com" },
        },
        {
          name = "dynv6_staging",
          provider = "dynv6",
          secret = "apikey of dynv6",
          domains = { "*.anotherdomain.com" },
        },
      },
      -- uncomment following to create anotherdomain.com in CN and *.anotherdomain.com in SAN
      -- wildcard_domain_in_san = true,
    })

默认情况下,该库尝试最多 5 分钟进行 DNS 传播。 如果 DNS 提供商的默认 TTL 比这更长,用户可能希望手动调整 challenge_start_delay 以等待更长时间。

resty.acme.autossl

可以将配置表传递给 resty.acme.autossl.init(),默认值为

    default_config = {
      -- accept term of service https://letsencrypt.openssl.ac.cn/repository/
      tos_accepted = false,
      -- if using the let's encrypt staging API
      staging = false,
      -- the path to account private key in PEM format
      account_key_path = nil,
      -- the account email to register
      account_email = nil,
      -- number of certificate cache, per type
      cache_size = 100,
      domain_key_paths = {
        -- the global domain RSA private key
        rsa = nil,
        -- the global domain ECC private key
        ecc = nil,
      },
      -- the private key algorithm to use, can be one or both of
      -- 'rsa' and 'ecc'
      domain_key_types = { 'rsa' },
      -- restrict registering new cert only with domain defined in this table
      domain_whitelist = nil,
      -- restrict registering new cert only with domain checked by this function
      domain_whitelist_callback = nil,
      -- interval to wait before retrying after failed certificate request
      failure_cooloff = 300,
      -- function that returns interval to wait before retrying after failed certificate request
      failure_cooloff_callback = nil,
      -- the threshold to renew a cert before it expires, in seconds
      renew_threshold = 7 * 86400,
      -- interval to check cert renewal, in seconds
      renew_check_interval = 6 * 3600,
      -- the store certificates
      storage_adapter = "shm",
      -- the storage config passed to storage adapter
      storage_config = {
        shm_name = 'acme',
      },
      -- the challenge types enabled
      enabled_challenge_handlers = { 'http-01' },
      -- time to wait before signaling ACME server to validate in seconds
      challenge_start_delay = 0,
      -- if true, the request to nginx waits until the cert has been generated and it is used right away
      blocking = false,
      -- if true, the certificate for domain not in whitelist will be deleted from storage
      enabled_delete_not_whitelisted_domain = false,
      -- the dict of dns providers, each provider should have following struct:
      -- {
      --   name = "prod_account",
      --   provider = "provider_name", -- "cloudflare" or "dynv6"
      --   secret  = "the api key or token",
      --   domains = { "example.com", "*.example.com" }, -- the list of domains that can be used with this provider
      -- }
      dns_provider_accounts = {},
      -- if enabled, wildcard domains like *.example.com will be created as SAN and CN will be example.com
      wildcard_domain_in_san = false,
    }

如果未指定 account_key_path,则每次 Nginx 重新加载配置时都会创建一个新的帐户密钥。 请注意,这可能会触发 Let's Encrypt API 的 新帐户速率限制

如果未指定 domain_key_paths,则会为每个证书生成一个新的私钥 (4096 位 RSA 和 256 位 prime256v1 ECC)。 请注意,生成此类密钥将阻塞工作线程,并且在熵较低的 VM 上尤为明显。

将配置表直接传递给 ACME 客户端作为第二个参数。 以下示例演示了如何使用除 Let's Encrypt 之外的 CA 提供程序,以及如何设置首选链。

    resty.acme.autossl.init({
        tos_accepted = true,
        account_email = "example@example.com",
      }, {
        api_uri = "https://acme.otherca.com/directory",
        preferred_chain = "OtherCA PKI Root CA",
      }
    )

另请参见下面的 "存储适配器"

使用分布式存储类型时,增加 challenge_start_delay 很有用,以便允许存储中的更改在周围传播。 当 challenge_start_delay 设置为 0 时,在开始验证挑战之前不会执行任何等待。

autossl.get_certkey

语法: certkey, err = autossl.get_certkey(domain, type?)

从存储中返回 domain 的 PEM 编码证书和私钥。 可选地接受 type 参数,该参数可以是 "rsa""ecc"; 如果省略,type 将默认为 "rsa"

resty.acme.client

client.new

语法: c, err = client.new(config)

创建一个 ACMEv2 客户端。

config 的默认值为

    default_config = {
      -- the ACME v2 API endpoint to use
      api_uri = "https://acme-v02.api.letsencrypt.org/directory",
      -- the account email to register
      account_email = nil,
      -- the account key in PEM format text
      account_key = nil,
      -- the account kid (as an URL)
      account_kid = nil,
      -- external account binding key id
      eab_kid = nil,
      -- external account binding hmac key, base64url encoded
      eab_hmac_key = nil,
      -- external account registering handler
      eab_handler = nil,
      -- storage for challenge
      storage_adapter = "shm",
      -- the storage config passed to storage adapter
      storage_config = {
        shm_name = "acme"
      },
      -- the challenge types enabled, selection of `http-01` and `tls-alpn-01`
      enabled_challenge_handlers = {"http-01"},
      -- select preferred root CA issuer's Common Name if appliable
      preferred_chain = nil,
      -- callback function that allows to wait before signaling ACME server to validate
      challenge_start_callback = nil,
      -- the dict of dns providers, each provider should have following struct:
      dns_provider_accounts = {},
    }

如果省略 account_kid,用户必须调用 client:new_account() 来注册新帐户。 请注意,使用相同的 account_key 时,client:new_account() 将返回先前注册的相同 kid

如果 CA 需要 "外部帐户绑定",用户可以设置 eab_kideab_hmac_key 来加载现有帐户,或者设置 account_emaileab_handler 来注册新帐户。 eab_hmac_key 必须是 base64 url 编码。 在后一种情况下,用户必须调用 client:new_account() 来注册新帐户。 eab_handler 必须是一个接受 account_email 作为参数并返回 eab_kideab_hmac_key 和错误(如果有)的函数。

    eab_handler = function(account_email)
      -- do something to register an account with account_email
      -- if err then
      --  return nil, nil, err
      -- end
      return eab_kid, eab_hmac_key
    end

以下 CA 提供程序的 EAB 处理程序受 lua-resty-acme 支持,用户无需实现自己的 eab_handler

preferred_chain 用于选择在其根 CA 中具有匹配公用名的链。 例如,用户可以使用 "ISRG Root X1" 来强制使用 Let's Encrypt 中的新默认链。 当未配置任何值或配置的名称未在任何链中找到时,将使用默认链。

challenge_start_callback 是一个回调函数,允许客户端在向 ACME 服务器发出信号以开始验证挑战之前等待。 这在挑战需要时间传播的分布式设置中很有用。 challenge_start_callback 接受 challenge_typechallenge_token。 客户端每秒调用一次此函数,直到它返回 true,指示挑战应开始; 如果未设置此 challenge_start_callback,则不会执行任何等待。

    challenge_start_callback = function(challenge_type, challenge_token)
      -- do something here
      -- if we are good
      return true
    end

另请参见下面的 "存储适配器"

client:init

语法: err = client:init()

初始化客户端,需要 cosocket API 的可用性。 此函数将登录或注册帐户。

client:order_certificate

语法: err = client:order_certificate(domain,...)

使用一个或多个域创建证书。 请注意,通配符域不受支持,因为它只能通过 dns-01 挑战验证。

client:serve_http_challenge

语法: client:serve_http_challenge()

服务 http-01 挑战。 常见的用例是将其作为 /.well-known 路径的 content_by_* 块。

client:serve_tls_alpn_challenge

语法: client:serve_tls_alpn_challenge()

服务 tls-alpn-01 挑战。 有关如何使用此处理程序,请参见 本节

存储适配器

存储适配器用于 autossl 或 acme client 中以存储临时或持久数据。 根据部署环境,目前有五个可供选择的存储适配器。 要实现自定义存储适配器,请参阅 此文档

file

基于文件系统的存储。 示例配置

    storage_config = {
        dir = '/etc/openresty/storage',
    }

如果省略 dir,将使用操作系统临时目录。

在使用 file 存储进行续订时,需要 luafilesystemluafilesystem-ffi

shm

基于 Lua 共享字典的存储。 请注意,此存储在 Nginx 重启(而不是重新加载)之间是易失的。 示例配置

    storage_config = {
        shm_name = 'dict_name',
    }

redis

基于 Redis 的存储。 默认配置为

    storage_config = {
        host = '127.0.0.1',
        port = 6379,
        database = 0,
        -- Redis authentication key
        auth = nil,
        ssl = false,
        ssl_verify = false,
        ssl_server_name = nil,
        -- namespace as a prefix of key
        namespace = "",
    }

需要 Redis >= 2.6.0,因为此存储需要 PEXPIRE

vault

基于 Hashicorp Vault 的存储。 仅支持 KV V2 后端。 默认配置为

    storage_config = {
        host = '127.0.0.1',
        port = 8200,
        -- secrets kv prefix path
        kv_path = "acme",
        -- timeout in ms
        timeout = 2000,
        -- use HTTPS
        https = false,
        -- turn on tls verification
        tls_verify = true
        -- SNI used in request, default to host if omitted
        tls_server_name = nil,
        -- Auth Method, default to token, can be "token" or "kubernetes"
        auth_method = "token"
        -- Vault token
        token = nil,
        -- Vault's authentication path to use
        auth_path =  "kubernetes",
        -- The role to try and assign
        auth_role = nil,
        -- The path to the JWT
        jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",
        -- Vault namespace
        namespace = nil,
    }

支持不同的身份验证方法

  • 令牌: 这是默认设置,允许在配置中传递一个字面上的 "token"

  • Kubernetes: 通过此方法,可以利用 Vault 为 Kubernetes 内置的身份验证方法。 这实际上是获取服务帐户令牌并验证它是否已由 Kubernetes CA 签名。 这里的主要好处是,配置文件不再公开您的令牌。

    以下配置适用于此: `lua -- 要使用的 Vault 的身份验证路径 auth_path = "kubernetes", -- 要尝试分配的角色 auth_role = nil, -- JWT 的路径 jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token", `

consul

基于 Hashicorp Consul 的存储。 默认配置为

    storage_config = {
        host = '127.0.0.1',
        port = 8500,
        -- kv prefix path
        kv_path = "acme",
        -- Consul ACL token
        token = nil,
        -- timeout in ms
        timeout = 2000,
    }

etcd

etcd 基于存储。 目前仅支持 v2 协议。 默认配置为

    storage_config = {
        http_host = 'http://127.0.0.1:4001',
        protocol = 'v2',
        key_prefix = '',
        timeout = 60,
        ssl_verify = false,
    }

Etcd 存储需要安装 lua-resty-etcd 库。可以使用 opm install api7/lua-resty-etcdluarocks install lua-resty-etcd 手动安装。

DNS 提供商

要创建自定义 DNS 提供商,请按照以下步骤操作

  • lib/resty/acme/dns_provider 下创建一个名为 route53.lua 的文件。

  • 实现以下函数签名:

    function _M.new(token)
      -- ... 
      return self
    end
    
    function _M:post_txt_record(fqdn, content)
      return ok, err
    end
    
    function _M:delete_txt_record(fqdn)
      return ok, err
    end

其中 token 是 apikey,fqdn 是要设置记录的 DNS 记录名称,content 是记录的值。

待办事项

  • autossl: ocsp staping

测试

通过运行 bash t/fixtures/prepare_env.sh 设置端到端测试环境。

然后运行 cpanm install Test::Nginx::Socket,然后运行 prove -r t

鸣谢

  • file 存储的改进由 @dbalagansky 提供。

  • 在“vault”存储中添加 Kubernetes 身份验证由 @UXabre 提供。

  • dns-01 挑战的初始支持由 @yuweizzz 提供。

版权和许可

此模块根据 BSD 许可证授权。

版权所有 (C) 2019,由 thenam153 <thenam153@gmail.com>。

保留所有权利。

在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用此软件,无论是否修改:

  • 源代码重新分发必须保留上述版权声明、此条件列表和以下免责声明。

  • 二进制形式的重新分发必须在随分发提供的文档和/或其他材料中复制上述版权声明、此条件列表和以下免责声明。

本软件由版权所有者和贡献者“按原样”提供,不附带任何明示或暗示的保证,包括但不限于对适销性和特定用途适用性的暗示保证。在任何情况下,版权所有者或贡献者均不对因使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务的费用;使用、数据或利润损失;或业务中断)负责,无论损害原因或责任理论如何,无论是在合同中、严格责任中还是侵权行为(包括疏忽或其他原因)中,即使已告知此类损害的可能性。

另请参见

作者

thenam153

许可证

3bsd

依赖关系

版本