通过 Socket 获取网站 SSL 证书及公钥

通过 php curl 请求网页并不能获取到证书信息,此时需要使用 ssl socket 获取证书内容。

// 创建 stream context
$context = stream_context_create([
    'ssl' => [
        'capture_peer_cert' => true,
        'capture_peer_cert_chain' => true,
    ],
]);

$resource = stream_socket_client("ssl://$domain:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

$ssl = $cert['options']['ssl'];
$resource = $ssl['peer_certificate'];

// 网站证书中只有公钥,通过 openssl_pkey_get_details 导出公钥

$ret = [
    'crt' => '',
    'pub' => '',
];

$pkey = openssl_pkey_get_public($resource);
$ret['pub'] = openssl_pkey_get_details($pkey)['key'];

openssl_x509_export($resource, $pem);
$ret['crt'] = $pem;

foreach ($ssl['peer_certificate_chain'] as $resource)
{
    openssl_x509_export($resource, $pem);
    $ret['crt'] .= "\n" . $pem;
}

// 保存 $ret['crt'] 为 domain.crt
// 保存 $ret['pub'] 为 domain.pub

return $ret;

验证证书中的公钥A是否正确,通过私钥导出公钥B,比较两者发现一致。

$domain = 'blog.zhengxianjun.com';
$port = '443';
// ...
$pub_a = $ret['pub'];

$private_key_path = '/conf/ssl/blog.zhengxianjun.com.key';

// 证书没有设置密码,$passphrase 为空字符串
$pkey = openssl_pkey_get_private(file_get_content($private_key_path), $passphrase = '');
$pub_b = openssl_pkey_get_details($pkey)['key'];

// 两者一致
var_dump($pub_a === $pub_b);

函数 stream_socket_client 还有一个用途是当知道服务器 IP 时,能获取到服务器可能可以使用的域名。

$resource = stream_socket_client("ssl://$ip:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

// 解析 X.509 格式证书
$info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);

// 获取证书中的可信域名列表
$domain = str_replace('DNS:', '', $info['extensions']['subjectAltName']);

以上可以看到获取网站证书并不能获得私钥。

在一些使用 CDN 的站点,如果使用了 HTTPS 同时又希望使用自有域名,是否需要将自己的私钥提供给 CDN 厂商呢?实际上证书路径与使用者名称(支持 https 的域名)并不需要一致。

也就是使用自有域名并进行 CDN 加速时不需要使用自有的 ssl 证书,只需将自己的 CDN 域名加到厂商证书的域名列表即可。

JavaScript Crypto-JS 使用手册

使用 Crypto-JS 可以非常方便地在 JavaScript 进行 MD5、SHA1、SHA2、SHA3、RIPEMD-160 哈希散列,进行 AES、DES、Rabbit、RC4、Triple DES 加解密。

基于 Crypto-JS 实现的在线加密解密工具 —— 在线哈希、在线散列 和 在线加密、在线解密

下面讲述如何使用 Crypto-JS。

Crypto-JS 源码托管在 Google Code,当前版本是 3.1。因为一般在国内也无法打开,所以把源代码分享到百度盘:http://pan.baidu.com/s/1bnyIGxT

Crypto-JS 原网址是 https://code.google.com/p/crypto-js/文中的英文描述均是原作者所有,Crypto-JS 采用的授权协议跟 MIT 类似,可以在任何场景放心使用。

文章最末附上 Crypto-JS 文件目录结构。

使用 Crypto-JS 进行哈希、散列

  • md5

引入 md5.js

<script src="cryptojs/rollups/md5.js"></script>

调用 md5 方法

var str = '123456';
CryptoJS.MD5(str);
  • pbkdf2

PBKDF2 is a password-based key derivation function. In many applications of cryptography, user security is ultimately dependent on a password, and because a password usually can’t be used directly as a cryptographic key, some processing is required.

A salt provides a large set of keys for any given password, and an iteration count increases the cost of producing keys from a password, thereby also increasing the difficulty of attack.

引入 pbkdf2.js

<script src="cryptojs/rollups/pbkdf2.js"></script>

调用 pbkdf2 方法

// 官方示例
var str = '123456';
var salt = CryptoJS.lib.WordArray.random(128/8);

var key128Bits = CryptoJS.PBKDF2(str, salt, { keySize: 128/32 });
var key256Bits = CryptoJS.PBKDF2(str, salt, { keySize: 256/32 });
var key512Bits = CryptoJS.PBKDF2(str, salt, { keySize: 512/32 });

var key512Bits1000Iterations = CryptoJS.PBKDF2("Secret Passphrase", salt, {
keySize: 512/32,
iterations: 1000
});
  • ripemd160

引入 ripemd160.js

<script src="cryptojs/rollups/ripemd160.js"></script>

调用 ripemd160 方法

var str = '123456';
CryptoJS.RIPEMD160(str);
  • sha 系列:sha1、sha2、sha3…

sha 具有一系列散列算法

The SHA hash functions were designed by the National Security Agency (NSA). SHA-1 is the most established of the existing SHA hash functions, and it’s used in a variety of security applications and protocols. Though, SHA-1’s collision resistance has been weakening as new attacks are discovered or improved.

SHA-256 is one of the four variants in the SHA-2 set. It isn’t as widely used as SHA-1, though it appears to provide much better security.

SHA-512 is largely identical to SHA-256 but operates on 64-bit words rather than 32.
CryptoJS also supports SHA-224 and SHA-384, which are largely identical but truncated versions of SHA-256 and SHA-512 respectively.

SHA-3 is the winner of a five-year competition to select a new cryptographic hash algorithm where 64 competing designs were evaluated.
SHA-3 can be configured to output hash lengths of one of 224, 256, 384, or 512 bits. The default is 512 bits.

引入 sha 相关 js

<script src="cryptojs/rollups/sha1.js"></script>
<script src="cryptojs/rollups/sha3.js"></script>
<script src="cryptojs/rollups/sha224.js"></script>
<script src="cryptojs/rollups/sha256.js"></script>
<script src="cryptojs/rollups/sha384.js"></script>
<script src="cryptojs/rollups/sha512.js"></script>

调用 sha 相关方法

var str = '123456';
CryptoJS.SHA1(str);

var str = '123456';
CryptoJS.SHA3(str);

// 配置输出的哈希长度
CryptoJS.SHA3(str, {outputLength : 224});
CryptoJS.SHA3(str, {outputLength : 256});
CryptoJS.SHA3(str, {outputLength : 384});
CryptoJS.SHA3(str, {outputLength : 512});

var str = '123456';
CryptoJS.SHA224(str);

var str = '123456';
CryptoJS.SHA256(str);

var str = '123456';
CryptoJS.SHA284(str);

var str = '123456';
CryptoJS.SHA512(str);
  • hmac 系列:hmac_md5、hmac_ripemd160、hmac_sha1、hmac_sha3…

引入 hmac 相关 js

<script src="cryptojs/rollups/hmac-sha1.js">
</script>
<script src="cryptojs/rollups/hmac-sha3.js">
</script>
<script src="cryptojs/rollups/hmac-sha224.js">
</script>
<script src="cryptojs/rollups/hmac-sha256.js">
</script>
<script src="cryptojs/rollups/hmac-sha384.js">
</script>
<script src="cryptojs/rollups/hmac-sha512.js">
</script>

调用 hmac 相关 方法

var str = '123456';
var password = 'password';

// Hmac 相关调用前都增加了 Hmac
CryptoJS.HmacMD5(str, password);
CryptoJS.HmacRIPEMD160(str, password);
CryptoJS.HmacSHA1(str, password);
CryptoJS.HmacSHA3(str, password);
CryptoJS.HmacSHA224(str, password);
CryptoJS.HmacSHA256(str, password);
CryptoJS.HmacSHA384(str, password);
CryptoJS.HmacSHA512(str, password);

使用 Crypto-JS 进行加密、解密

Crypto-JS 相关 encrypt 函数会并不直接返回字符串,需要调用返回对象的 toString 方法,或者通过 Crypto-JS 转码才能得到真实的结果。

  • AES

引入 aes.js

<script src="cryptojs/rollups/aes.js"></script>

调用 CryptoJS.AES

// 官方示例, 每次输出的密文都不一样
CryptoJS.AES.encrypt("Message", "Secret Passphrase");

// 正确用法

var str = '123456';
// 密钥 16 位
var key = '0123456789abcdef';
// 初始向量 initial vector 16 位
var iv = '0123456789abcdef';
// key 和 iv 可以一致

key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);

var encrypted = CryptoJS.AES.encrypt(str, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为字符串
encrypted = encrypted.toString();

// mode 支持 CBC、CFB、CTR、ECB、OFB, 默认 CBC
// padding 支持 Pkcs7、AnsiX923、Iso10126
// 、NoPadding、ZeroPadding, 默认 Pkcs7, 即 Pkcs5

var decrypted = CryptoJS.AES.decrypt(encrypted, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为 utf8 字符串
decrypted = CryptoJS.enc.Utf8.stringify(decrypted);

// 引入其他加密模式, 填充模式需要引入对应的 js 文件
/*
cryptojs/components/mode-cfb-min.js
cryptojs/components/mode-ctr-min.js
cryptojs/components/mode-ecb-min.js
cryptojs/components/mode-ofb-min.js
cryptojs/components/pad-ansix923-min.js
cryptojs/components/pad-iso10126-min.js
cryptojs/components/pad-iso97971-min.js
cryptojs/components/pad-nopadding-min.js
*/
  • DES、Triple DES

DES、Triple DES 的调用形式与 AES 一致。

引入 triple.js, DES 和 Triple DES 都定义在 tripledes.js

<script src="cryptojs/rollups/tripledes.js"></script>

调用 CryptoJS.DES, CryptoJS.TripleDES

var str = '123456';
var key = '0123456789abcdef';
var iv = '0123456789abcdef';

key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);

// DES 加密
var encrypted = CryptoJS.DES.encrypt(str, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为字符串
encrypted = encrypted.toString();

// DES 解密
var decrypted = CryptoJS.DES.decrypt(encrypted, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为 utf8 字符串
decrypted = CryptoJS.enc.Utf8.stringify(decrypted);

// Triple DES 加密
var encrypted = CryptoJS.TripleDES.encrypt(str, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为字符串
encrypted = encrypted.toString();

// Triple DES 解密
var decrypted = CryptoJS.TripleDES.decrypt(encrypted, key, {
	iv: iv,
	mode: CryptoJS.mode.CBC,
	padding: CryptoJS.pad.Pkcs7
});

// 转换为 utf8 字符串
decrypted = CryptoJS.enc.Utf8.stringify(decrypted);
  • Rabbit、RC4

Rabbit、RC4 调用方式一致,不支持 mode、padding。

引入 rabbit.js

<script src="cryptojs/rollups/rabbit.js"></script>

调用 CryptoJS.Rabbit

var str = '123456';
var key = '0123456789abcdef';
var iv = '0123456789abcdef';

key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);

var encrypted = CryptoJS.Rabbit.encrypt(str, key, {
	iv: iv
});

// 转换为字符串
encrypted = encrypted.toString();

var decrypted = CryptoJS.Rabbit.decrypt(encrypted, key, {
	iv: iv
});

// 转换为 utf8 字符串
decrypted = CryptoJS.enc.Utf8.stringify(decrypted);

引入 rc4.js

<script src="cryptojs/rollups/rc4.js"></script>

调用 CryptoJS.RC4

var str = '123456';
var key = '0123456789abcdef';
var iv = '0123456789abcdef';

key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);

var encrypted = CryptoJS.RC4.encrypt(str, key, {
	iv: iv
});

// 转换为字符串
encrypted = encrypted.toString();

var decrypted = CryptoJS.RC4.decrypt(encrypted, key, {
	iv: iv
});

// 转换为 utf8 字符串
decrypted = CryptoJS.enc.Utf8.stringify(decrypted);

Crypto-JS 文件目录结构

  • cryptojs
    • components
      • aes.js
      • cipher-core.js
      • core.js
      • enc-base64.js
      • enc-utf16.js
      • evpkdf.js
      • format-hex.js
      • hmac.js
      • lib-typedarrays.js
      • md5.js
      • mode-cfb.js
      • mode-ctr.js
      • mode-ctr-gladman.js
      • mode-ecb.js
      • mode-ofb.js
      • pad-ansix923.js
      • pad-iso10126.js
      • pad-iso97971.js
      • pad-nopadding.js
      • pad-zeropadding.js
      • pbkdf2.js
      • rabbit.js
      • rabbit-legacy.js
      • rc4.js
      • ripemd160.js
      • sha1.js
      • sha3.js
      • sha224.js
      • sha256.js
      • sha384.js
      • sha512.js
      • tripledes.js
      • x64-core.js
    • rollups
      • aes.js
      • hmac-md5.js
      • hmac-ripemd160.js
      • hmac-sha1.js
      • hmac-sha3.js
      • hmac-sha224.js
      • hmac-sha256.js
      • hmac-sha384.js
      • hmac-sha512.js
      • md5.js
      • pbkdf2.js
      • rabbit.js
      • rabbit-legacy.js
      • rc4.js
      • ripemd160.js
      • sha1.js
      • sha3.js
      • sha224.js
      • sha256.js
      • sha384.js
      • sha512.js
      • tripledes.js

cryptojs/components 目录下的所有文件都会对应一个压缩文件,比如 cryptojs/components/aes.js 同时会有一个 cryptojs/components/aes-min.js 文件。

cryptojs/rollups 目录下的所有文件都已经压缩完毕,每个文件都可以单独调用。

Web安全之X-Frame-Options

为了避免网页被 frame 或者 iframe 嵌套,可以设置 Frame-Options 禁用。

Frame-Options 目前还未完全成为标准,仍需要使用 X-Frame-Options。

X-Frame-Options 值列表

DENY 拒绝在 frame 或 iframe 中加载网页
SAMEORIGIN 允许在同域 frame 和 iframe 中加载网页
ALLOW-FROM uri 允许在指定域下的 frame 和 iframe 中加载网页,支持并不完整

例如在 www.main.com frame-options.php 中增加 X-Frame-Options header,然后在 www.third.com frame.php 中增加 iframe,指向 frame-options.php,效果如下:

Frame-Options

代码示例:

www.main.com

// 文件 frame-options.php
<?php header('X-Frame-Options: DENY');?>
You cannot see this page.

www.third.com

// 文件 frame.php
<iframe src="http://www.main.com/frame-options.php"></iframe>

Web安全之CSRF

CSRF 全称是 Cross-Site Request Forgery,指攻击者盗用了用户的身份,以用户的身份发送恶意请求。

CSRF 攻击流程

  1. User 登录网站 A(存在 CSRF 攻击漏洞的网站)
  2. User 在没有网站 A 的情况下,使用同一个浏览器访问危险网站 B
  3. 网站 B 模拟 User,发送恶意的 GET 或者 POST 请求(通过 iframe、js 等)到网站 A。比如让网站 A 删除用户数据
  4. 网站 A 在没有防范的情况下,破坏或者泄露了用户数据

网站 A 防范 CSRF 攻击的简单方式是在用户提交表单时,增加一个令牌验证。

令牌验证

在网站 A 增加简单令牌

<?php
function gen_csrf_token()
{
	session_start();
	return md5(session_id());
}

function verify_csrf_token($token)
{
	session_start();
	return $token = md5(session_id());
}
?>
<form action="/user/update" method="post">
	<input name="csrf_token" value="<?=gen_csrf_token()?>"/>
	<input name="sex" value="man"/>
	<input type="submit"/>
</form>

当表单提交时,执行 verify_csrf_token 判断是否令牌是否正确,继而认为不是第三方网站伪造的请求。

HTTP_REFERER 验证

验证 http_referer 也能够从一定程度上降低 CSRF 攻击风险。现代浏览器是不允许修改 HTTP_REFERER 头信息的。

Web安全之CSP

CSP 全称是 Content Security Policy,控制浏览器在加载网络资源时,告知浏览器哪些资源是可信的、安全的。可以有效地降低和杜绝 XSS 攻击。

如何产生 XSS 攻击,可以参考 http://blog.zhengxianjun.com/2015/web-security-xss/

有时候即便整个网站增加了 XSS 防注入,也可能在迭代过程中存在遗漏。

浏览器不能区分哪些资源是安全的,一旦下载之后就会执行。(有些浏览器可以识别简单的 XSS 注入)。

CSP 就是为了告诉浏览器哪些资源是可信任的、安全的,通过白名单去加载网络资源和执行。这样即使网页存在 XSS 注入,出现损失的可能性也会大大地降低。

比如:

Content-Security-Policy: script-src https://s.zhengxianjun.com

那么浏览器只会信任来自 s.zhengxianjun.com 的脚本,这样即使网站被注入了第三方站点脚本或者内联,也不会执行,那 XSS 攻击也就失效了。

代码示例:

<?php header('Content-Security-Policy: script-src https://s.zhengxianjun.com');?>
<script type="text/javascript" src="https://code.jquery.com/jquery.js"></script>
// 支持 CSP 则不会加载来自 jquery.com 的脚本

以上代码分别在 IE、Chrome、Firefox 中的效果。

CSP IE

在 IE10 中,似乎没有支持 CSP,jquery.js 被正常加载和执行。这是因为早期 CSP 没有规范,IE 识别 X-Content-Security-Policy,但并不是完整支持。

CSP Chrome

Chrome 正确识别了 CSP,并且说明了原因。

CSP Firefox

Firefox 正确识别了 CSP,并且说明了原因。

CSP 是帮助浏览器创建了一个网络资源白名单。

CSP 规则列表

Content-Security-Policy: script-src 'self' s.zhengxianjun.com;style-src 'self';img-src 'self'
  • 多个指令之间用英文分号隔开;
  • 指令的值不是域名时需要引号引起来;
  • 指令重复以第一个为准。
指令 说明
default-src 定义资源默认加载策略
connect-src 定义 Ajax、WebSocket 等加载策略
font-src 定义 Font 加载策略
frame-src 定义 Frame 加载策略
img-src 定义图片加载策略
media-src 定义 <audio>、<video> 等引用资源加载策略
object-src 定义 <applet>、<embed>、<object> 等引用资源加载策略
script-src 定义 JS 加载策略
style-src 定义 CSS 加载策略
sandbox 值为 allow-forms,对资源启用 sandbox
report-uri 值为 /report-uri,提交日志

以上指令除了 sandbox、report-uri 是比较特殊的值,其余指令可以接收 ‘self’、data、域名 等值。

指令示例

示例 说明
script-src 允许任何资源
script-src ‘none’ 不允许任何资源
script-src ‘self’ 允许同源资源(协议、域名、端口均相同)
script-src ‘unsafe-eval’ 允许动态脚本,如 eval、setTimeout、new Function
script-src ‘unsafe-inline’ 允许内联资源
script-src s.zhengxianjun.com 允许某个域名下的资源
script-src *.zhengxianjun.com 允许子域名下的资源
script-src https 允许 https 资源
script-src https://zhengxianjun.com 允许某个域名下的 https 资源
img-src data 允许 data: 协议资源

report-uri 会接收到一个 post 请求,包含 CSP 阻止的详细信息。

虽然 CSP 能够有效降低 XSS 攻击,但是不要指望它可以完全杜绝 XSS 攻击。

为了让产品能够顺利过渡到 CSP,可以通过 Content-Security-Policy-Report-Only 先收集 CSP 报告,根据报告修正被阻止的正确资源,再启用 CSP。使用了 Content-Security-Policy-Report-Only 则一定要配置 report-uri。

示例:

Content-Security-Policy-Report-Only: script-src 'self'; report-uri https://www.zhengxianjun.com/csp/report