Showing preview only (791K chars total). Download the full file or copy to clipboard to get everything.
Repository: cangSDARM/illustrate
Branch: master
Commit: ee3b5c8ce7a5
Files: 154
Total size: 638.1 KB
Directory structure:
gitextract_b3_7o8a6/
├── .github/
│ └── workflows/
│ └── gh-pages.yml
├── .gitignore
├── .npmrc
├── .parcelrc
├── .prettierrc
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── frombootstrap.css
├── index.html
├── package.json
├── print.js
├── printmode.css
├── public/
│ ├── .nojekyll
│ ├── 404.html
│ └── sitemap.xml
└── src/
├── App.jsx
├── DTLS/
│ ├── clientApplicationDataDatagram.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientHandshakeFinishedDatagram.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientHelloDatagram.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverACKDatagram.json
│ ├── serverAlertDatagram.json
│ ├── serverApplicationDataDatagram.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverCertVerifyDatagram.json
│ ├── serverCertificateDatagram.json
│ ├── serverEncryptedExtensionsDatagram.json
│ ├── serverHandshakeFinishedDatagram.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHelloDatagram.json
│ └── serverKeyExchangeGeneration.json
├── Datagram/
│ ├── index.jsx
│ └── style.module.css
├── Footer/
│ └── index.jsx
├── Header/
│ ├── index.jsx
│ └── style.module.css
├── Intro/
│ ├── index.jsx
│ └── style.module.css
├── QUIC/
│ ├── clientAck.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientApplicationPacket1.json
│ ├── clientApplicationPacket2.json
│ ├── clientHandshake1.json
│ ├── clientHandshake2.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientInitialKeysCalc.json
│ ├── clientInitialPacket.json
│ ├── clientKeyExchangeGeneration.json
│ ├── clientTLSHandshakeFinished.json
│ ├── index.js
│ ├── padding.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverApplicationPacket1.json
│ ├── serverApplicationPacket2.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHandshakePacket1.json
│ ├── serverHandshakePacket2.json
│ ├── serverHandshakePacket3.json
│ ├── serverInitialKeysCalc.json
│ ├── serverInitialPacket.json
│ ├── serverKeyExchangeGeneration.json
│ ├── serverTLSHandshakeFinished.json
│ ├── tls-certificate.json
│ ├── tls-certificateVerify.json
│ ├── tls-clientHello.json
│ ├── tls-encryptedExtensions.json
│ └── tls-serverHello.json
├── RecOuter/
│ ├── Annotations/
│ │ ├── context.js
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── CodeSample/
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── Math/
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── Table/
│ │ └── index.jsx
│ ├── X25519/
│ │ ├── Calculator/
│ │ │ ├── PublicKeyMultiplier.jsx
│ │ │ ├── SecretKeyMultiplier.jsx
│ │ │ ├── YCoordinate.jsx
│ │ │ ├── index.jsx
│ │ │ ├── style.module.css
│ │ │ └── utils.js
│ │ ├── Flex/
│ │ │ ├── index.jsx
│ │ │ └── style.module.css
│ │ ├── QA/
│ │ │ ├── index.jsx
│ │ │ └── style.module.css
│ │ ├── curve.js
│ │ ├── field.js
│ │ └── index.js
│ ├── index.jsx
│ ├── style.module.css
│ └── utils.jsx
├── TLS12/
│ ├── clientApplicationData.json
│ ├── clientChangeCipherSpec.json
│ ├── clientCloseNotify.json
│ ├── clientEncryptionKeysGeneration.json
│ ├── clientHandshakeFinished.json
│ ├── clientHello.json
│ ├── clientKeyExchange.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverApplicationData.json
│ ├── serverCertificate.json
│ ├── serverChangeCipherSpec.json
│ ├── serverEncryptionKeysGeneration.json
│ ├── serverHandshakeFinished.json
│ ├── serverHello.json
│ ├── serverHelloDone.json
│ ├── serverKeyExchange.json
│ └── serverKeyExchangeGeneration.json
├── TLS13/
│ ├── clientApplicationData.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientChangeCipherSpec.json
│ ├── clientHandshakeFinished.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientHello.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverApplicationData.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverCertVerify.json
│ ├── serverCertificate.json
│ ├── serverChangeCipherSpec.json
│ ├── serverEncryptedExtensions.json
│ ├── serverHandshakeFinished.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHello.json
│ ├── serverKeyExchangeGeneration.json
│ ├── serverNewSessionTicket1.json
│ ├── serverNewSessionTicket2.json
│ ├── wrappedRecord1.json
│ ├── wrappedRecord2.json
│ ├── wrappedRecord3.json
│ ├── wrappedRecord4.json
│ ├── wrappedRecord5.json
│ ├── wrappedRecord6.json
│ ├── wrappedRecord7.json
│ ├── wrappedRecord8.json
│ └── wrappedRecord9.json
├── X25519/
│ ├── handsOn.json
│ ├── index.js
│ ├── mathOnTheCurve.json
│ ├── overview.json
│ └── q&a.json
├── common.css
├── context/
│ └── slugger.js
├── hard-encoded.css
├── illustrated.css
├── index.js
├── router.js
└── utils.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/gh-pages.yml
================================================
name: deploy gh-pages
on:
pull_request:
types: [closed]
branches: [master]
push:
branches: [master]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
- name: Environment Setup 🔧
uses: actions/setup-node@v4
with:
node-version: "18.x"
- name: Cache dependencies 🍪
uses: actions/cache@v4
id: cache
with:
path: |
**/.npm
**/node_modules/
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies without Proxy 📁
# only install when cache miss or package.json changed
if: steps.cache.outputs.cache-hit != 'true'
run: npm --proxy=null --https_proxy=null --strict-ssl=true install
- name: Cache buildings 🏗
uses: actions/cache@v4
id: building
with:
path: |
**/.parcel-cache/
**/dist/
key: ${{ runner.os }}-build
- name: Build 🚧
run: npm run build
- name: Deploy 🚀
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: ./dist
================================================
FILE: .gitignore
================================================
build
dist
node_modules
.parcel-cache
*.local
yarn.lock
package-lock.json
================================================
FILE: .npmrc
================================================
registry = https://registry.npmmirror.com/
================================================
FILE: .parcelrc
================================================
{
"extends": [
"@parcel/config-default"
],
"reporters": [
"...",
"parcel-reporter-static-files-copy"
]
}
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "es5",
"singleQuote": false,
"tabWidth": 2,
"semi": true,
"endOfLine": "lf"
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": ["middleboxes", "middlebox", "DTLS", "QUIC"]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Allen Lee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# illustrate QUIC, TLS 1.2, TLS 1.3, DTLS 中文翻译
图解 QUIC, TLS 1.2, TLS 1.3, DTLS 协议的连接及会话过程
翻译自:
- https://quic.xargs.org/
- https://dtls.xargs.org/
- https://tls12.xargs.org/
- https://tls13.xargs.org/
- https://x25519.xargs.org/
原作者 [syncsynchalt](https://github.com/syncsynchalt), 译者 [AllenLee](https://github.com/cangSDARM)
## 进度
- [x] QUIC
- [x] DTLS
- [x] TLS 1.2
- [x] TLS 1.3
- [x] x25519
================================================
FILE: frombootstrap.css
================================================
/* everything we wanted from bootstrap but nothing more */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 1em;
}
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1,
.h1 {
font-size: 2.5rem;
}
h2,
.h2 {
font-size: 2rem;
}
h3,
.h3 {
font-size: 1.75rem;
}
h4,
.h4 {
font-size: 1.5rem;
}
h5,
.h5 {
font-size: 1.25rem;
}
h6,
.h6 {
font-size: 1rem;
}
pre {
display: block;
font-size: 87.5%;
color: #212529;
}
table {
background-color: transparent;
border-spacing: 0;
border-collapse: collapse;
}
td, th {
padding: 0;
}
th {
text-align: left;
}
.table {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
}
.table > thead > tr > th,
.table > tbody > tr > th,
.table > tfoot > tr > th,
.table > thead > tr > td,
.table > tbody > tr > td,
.table > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #ddd;
}
.table > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #ddd;
}
.table > caption + thead > tr:first-child > th,
.table > colgroup + thead > tr:first-child > th,
.table > thead:first-child > tr:first-child > th,
.table > caption + thead > tr:first-child > td,
.table > colgroup + thead > tr:first-child > td,
.table > thead:first-child > tr:first-child > td {
border-top: 0;
}
.table > tbody + tbody {
border-top: 2px solid #ddd;
}
.table .table {
background-color: #fff;
}
.table-condensed > thead > tr > th,
.table-condensed > tbody > tr > th,
.table-condensed > tfoot > tr > th,
.table-condensed > thead > tr > td,
.table-condensed > tbody > tr > td,
.table-condensed > tfoot > tr > td {
padding: 5px;
}
table col[class*="col-"] {
position: static;
display: table-column;
float: none;
}
table td[class*="col-"],
table th[class*="col-"] {
position: static;
display: table-cell;
float: none;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.container {
padding-left: 5px;
padding-right: 5px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="cn" dir="ltr">
<head>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script checks to see if a redirect is present in the query string,
// converts it back into the correct url and adds it to the
// browser's history using window.history.replaceState(...),
// which won't cause the browser to attempt to load the new url.
// When the single page app is loaded further down in this file,
// the correct url will be waiting in the browser's history for
// the single page app to route accordingly.
(function (l) {
if (l.search[1] === '/') {
var decoded = l.search.slice(1).split('&').map(function (s) {
return s.replace(/~and~/g, '&')
}).join('?');
var path = l.pathname.slice(0, -1) + decoded + l.hash;
window.history.replaceState({ path }, null,
path,
);
}
}(window.location));
</script>
<meta charset="utf-8" />
<title>图解</title>
<meta name="description" content="图解 QUIC, TLS 1.2, TLS 1.3, DTLS 协议的连接及会话过程" />
<link rel='canonical' href='https://cangsdarm.github.io/illustrate/' />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="./frombootstrap.css?bust" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<meta content="图解,QUIC,TLS 1.2,TLS 1.3,DTLS,协议连接,会话过程" name="Keywords" />
<meta name="google-site-verification" content="Qa9IAETG_rUMzy5ZXHjOxjAK7awElBSO6p_XVLGzuZk" />
</head>
<body>
<div id="app"></div>
<script type="module" src="src/index.js"></script>
<script type="module" async src="./print.js"></script>
<a class="print-mode" href="#print" onclick="globalThis.illustrate.printMode()">
[print, TODO]
</a>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "illustrate",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "parcel index.html",
"format": "prettier --write \"./src/**/*.{js,jsx,css,scss}\"",
"rm:cache": "rimraf .parcel-cache && rimraf ./dist",
"build": "npm run rm:cache && parcel build index.html --public-url ./"
},
"author": "AllenLee<648384410li@gmail.com>",
"license": "MIT",
"staticFiles": {
"staticPath": "public"
},
"devDependencies": {
"parcel": "^2.8.3",
"parcel-reporter-static-files-copy": "^1.5.0",
"prettier": "^2.8.7",
"process": "^0.11.10",
"rimraf": "^5.0.1"
},
"dependencies": {
"clsx": "^1.2.1",
"github-slugger": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-wrap-balancer": "^0.4.0"
}
}
================================================
FILE: print.js
================================================
globalThis.illustrate = {
printMode: () => {
// add printmode css
let inject = document.createElement("link");
inject.setAttribute("rel", "stylesheet");
inject.setAttribute("href", "printmode.css");
document.head.appendChild(inject);
// open everything up
// TODO:
// [].forEach.call(document.querySelectorAll(".record, .calculation"), (el) => {
// el.classList.add("selected");
// el.classList.add("annotate");
// });
// [].forEach.call(document.querySelectorAll("codesample"), (el) => {
// el.classList.add("show");
// });
[].forEach.call(document.querySelectorAll("*"), (el) => {
el.onclick = null;
});
}
};
================================================
FILE: printmode.css
================================================
/* print mode */
@media (min-width: 0) {
.container {
max-width: none !important;
margin: 5px 0 !important;
}
}
*,
*:hover {
color: #000 !important;
background-color: #fff !important;
text-shadow: none !important;
}
.illustration {
display: none !important;
}
button {
display: none !important;
}
.client,
.server {
background-color: #fff !important;
border: 1px solid black !important;
box-shadow: none !important;
max-width: none !important;
margin: 1em 0;
}
.rec-label:after {
content: "" !important;
}
.string > .explanation,
.decryption > .explanation {
background-color: #fff !important;
box-shadow: none !important;
border: 2px solid black !important;
}
.record.annotate .string > .explanation:before {
display: none;
}
pre code {
border-radius: 0 !important;
border: 2px solid black !important;
white-space: pre-wrap !important;
}
a:after {
content: " [link]";
}
a.no-show:after {
content: "";
}
.print-mode {
display: none;
}
.header {
display: none;
}
================================================
FILE: public/.nojekyll
================================================
================================================
FILE: public/404.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
// Otherwise, leave pathSegmentsToKeep as 0.
var pathSegmentsToKeep = 1;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>
================================================
FILE: public/sitemap.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://cangsdarm.github.io/illustrate/</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://cangsdarm.github.io/illustrate/quic</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://cangsdarm.github.io/illustrate/dtls</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://cangsdarm.github.io/illustrate/tls13</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://cangsdarm.github.io/illustrate/tls12</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
================================================
FILE: src/App.jsx
================================================
import React from "react";
import "./common.css";
import "./hard-encoded.css";
import "./illustrated.css";
import Header from "./Header";
import Intro from "./Intro";
import { routers, base } from "./router";
import RecOuter from "./RecOuter";
import { SluggerContextProvider } from "./context/slugger";
import Datagram from "./Datagram";
import Footer from "./Footer";
const App = () => {
const [curPage, setCurPage] = React.useState();
const [loading, setLoading] = React.useState(false);
const [JSON, setJSON] = React.useState({});
React.useEffect(() => {
curPage?.json().then((json) => {
setJSON(json?.default || json);
setLoading(false);
});
}, [curPage]);
return (
<>
<Header
routers={routers}
base={base}
onRouterChange={(rt) => {
window.document.title = rt.title;
setLoading(true);
setCurPage(rt);
}}
/>
{loading ? (
<div>loading...</div>
) : (
<SluggerContextProvider>
<Intro {...JSON?.intro} />
{(function () {
let datagramMeta = { name: "", children: [], length: 0 };
return JSON?.sections?.map((sec) => {
const { type, tags, datagram, ...restSec } = sec;
const key = sec.id || sec.label;
if (type === "Datagram") {
datagramMeta.name = sec.label;
datagramMeta.length = datagram;
datagramMeta.children = [];
return undefined;
}
if (datagramMeta.name) {
let newLen = datagramMeta.children.length;
if (newLen < datagramMeta.length) {
newLen = datagramMeta.children.push(
<RecOuter key={sec.label} types={tags} {...restSec} />
);
}
if (newLen < datagramMeta.length) {
return undefined;
} else {
const { name, children } = datagramMeta;
datagramMeta = {};
return (
<Datagram key={key} label={name} children={children} />
);
}
}
return <RecOuter key={key} types={tags} {...restSec} />;
});
})()}
<Footer {...JSON.ending} />
</SluggerContextProvider>
)}
</>
);
};
export default App;
================================================
FILE: src/DTLS/clientApplicationDataDatagram.json
================================================
[
"客户端发送数据:字符串\"ping\"",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2f"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2f 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"11",
"加密序列指示(Encryption epoch 3),现在密钥是会话时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "68 3f"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 00"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"客户端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client record number key\" from application keys calc step above\n$ key=5cb5bd8bac29777c650c0dde22d16d47\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=7d72278b6c649f1e7b56b3cad411faf7\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\n683f\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 15"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 DTLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 15 - 表示 DTLS 记录长度为 0x15(21) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "7d 72 27 8b 6c"
},
["这些数据使用客户端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "64 9f 1e 7b 56 b3 ca d4 11 fa f7 bd 51 8b fb 15"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端会话密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Client Application Keys Calc\" step\n$ key=9ba90dbce8857bc1fcb81d41a0465cfe\n$ iv=682219974631fa0656ee4eff\n### from this record\n$ recdata=2f00000015\n$ authtag=649f1e7b56b3cad411faf7bd518bfb15\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 70 69 6e 67 17 |ping.|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据",
{
"props": {
"className": "bytes encrypted"
},
"content": "70 69 6e 67"
},
["字符串\"ping\""]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "17"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "17 - 类型为 0x17(23), 会话数据"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/clientApplicationKeysCalc.json
================================================
[
"客户端现在也可以计算应用会话时的密钥了。如果计算正确,结果应和服务器端的一致:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"children": [
"客户端会话密钥: ",
{
"Tag": "code",
"content": "9ba90dbce8857bc1fcb81d41a0465cfe"
}
]
},
{
"Tag": "li",
"children": [
"客户端会话向量: ",
{
"Tag": "code",
"content": "682219974631fa0656ee4eff"
}
]
},
{
"Tag": "li",
"children": [
"客户端序号保护密钥: ",
{
"Tag": "code",
"content": "5cb5bd8bac29777c650c0dde22d16d47"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话密钥: ",
{
"Tag": "code",
"content": "2b65fffbbc8189474aa2003c43c32d4d"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话向量: ",
{
"Tag": "code",
"content": "582f5a11bdaf973fe3ffeb4e"
}
]
},
{
"Tag": "li",
"children": [
"服务器端序号保护密钥: ",
{
"Tag": "code",
"content": "57ba02596c6a1352d7fe8416c7e17d5a"
}
]
}
]
}
]
================================================
FILE: src/DTLS/clientHandshakeFinishedDatagram.json
================================================
[
"为了验证握手成功且没有被篡改过,客户端和服务器端一样,需要创建一些验证数据给服务器端确认。验证数据是基于所有握手信息的哈希值计算得到。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2e"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2e 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"10",
"加密序列指示(Encryption epoch 2),现在密钥是握手时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "c2 48"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 00"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"客户端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client record number key\" from handshake keys calc step above\n$ key=beed6218676635c2cb46a45694144fec\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=8a2cd52d5000f8786afb47cdf0b8f2b8\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\nc248\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 3d"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 TLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 3d - 表示 TLS 记录长度为 0x3d(61) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手密钥\"加密"
},
"content": "8a 2c d5 2d 50 00 f8 78 6a fb 47 cd f0 b8 f2 b8 13 42 b0 0c 43 dc e6 4b 1d 01 94 d2 e2 01 f6 81 75 09 78 52 8b be 26 af 79 61 24 01 c0"
},
["这些数据使用服务器端的\"握手密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "07 a2 c5 f7 5f 7c ff b7 46 5b c0 1d 23 d8 51 1f"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端握手密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Client Handshake Keys Calc\" step\n$ key=6caa2633d5e48f10051e69dc45549c97\n$ iv=106dc6e393b7a9ea8ef29dd7\n### from this record\n$ recdata=2e0000003d\n$ authtag=07a2c5f75f7cffb7465bc01d23d8511f\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 14 00 00 20 00 01 00 00 00 00 00 20 6f 28 01 39\n00000010 6b 0e 90 eb b4 a3 ba 38 4a bc fc 6b 24 20 1a bd\n00000020 81 b3 16 b2 39 1d a3 78 37 7f ac f5 16"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"握手消息头",
{
"props": {
"className": "bytes encrypted"
},
"content": "14 00 00 20"
},
[
"每个握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "14 - 握手消息类型 0x14 (finished)"
},
{
"Tag": "li",
"content": "00 00 20 - 紧接着的握手消息数据的长度 0x20 (32) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 01 00 00 00 00 00 20"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 01 - DTLS 序列号 0x1(1)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 00 20 - 表明之后的记录碎片的长度为 0x20(32) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"验证数据",
{
"props": {
"className": "bytes encrypted"
},
"content": "6f 28 01 39 6b 0e 90 eb b4 a3 ba 38 4a bc fc 6b 24 20 1a bd 81 b3 16 b2 39 1d a3 78 37 7f ac f5"
},
[
"使用 \"客户端生成握手密钥\" 步骤中的客户端密文和在这之前的每个握手记录(ClientHello 到 服务器端握手结束)的 SHA256 哈希值生成。",
{
"Tag": "pre",
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "finished_key = HKDF-Expand-Label(key: client_secret, label: \"finished\", ctx: \"\", len: 32)\nfinished_hash = SHA256(Client Hello ... Server Handshake Finished)\nverify_data = HMAC-SHA256(key: finished_key, msg: finished_hash)"
}
]
},
{
"children": [
"在命令行中使用",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/hkdf-dtls.sh"
},
"content": "原作者制作的 HKDF 命令行脚本"
},
",你也可以自己试试:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### find the hash of the conversation to this point, excluding\n### cleartext record headers, DTLS-only record headers,\n### or 1-byte decrypted record trailers\n$ fin_hash=$((\n cat record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-encext | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cert | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-sfin | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n ) | openssl sha256)\n$ cht_secret=33e472fb8d821b0193314626bebee307ccbd1aeb3d3a17ba468888ffc5246da1\n$ fin_key=$(./hkdf-dtls expandlabel $cht_secret \"finished\" \"\" 32)\n$ echo $fin_hash | xxd -r -p | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key\n\n6f2801396b0e90ebb4a3ba384abcfc6b24201abd81b316b2391da378377facf5"
}
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "16"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 类型为 0x16(22), 握手记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/clientHandshakeKeysCalc.json
================================================
[
"客户端现在也拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,客户端使用了以下信息:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "服务器端公钥(提取自 ServerHello 数据报)"
},
{
"Tag": "li",
"content": "客户端私钥(之前准备密钥交换时生成)"
},
{
"Tag": "li",
"content": "ClientHello 和 ServerHello 的 SHA256 哈希值"
}
]
},
"首先,客户端需要找到共享的密文(shared secret),即密钥交换步骤的最终值。客户端通过使用 curve25519 算法将服务器端的公钥乘以客户端的私钥(椭圆曲线乘法的特性将使得计算结果与服务器在其计算中得到的结果一致),即可发现 32 字节的最终值是:",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624"
}
]
},
{
"children": [
"你可以使用",
{
"Tag": "a",
"props": { "href": "https://quic.xargs.org/files/curve25519-mult.c" },
"content": "原作者的脚本"
},
"快速验证结果:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ cc -o curve25519-mult curve25519-mult.c\n$ ./curve25519-mult client-ephemeral-private.key server-ephemeral-public.key | hexdump\n\n0000000 df 4a 29 1b aa 1e b7 cf a6 93 4b 29 b4 74 ba ad\n0000010 26 97 e2 9f 1f 92 0d cc 77 c8 a0 a0 88 44 76 24"
}
},
"由于上面的计算结果与服务器端计算结果相同,且剩下的计算过程也相同,因此算出来的数值也应该是相同的:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"children": [
"客户端握手时密钥: ",
{
"Tag": "code",
"content": "6caa2633d5e48f10051e69dc45549c97"
}
]
},
{
"Tag": "li",
"children": [
"客户端握手时向量: ",
{
"Tag": "code",
"content": "106dc6e393b7a9ea8ef29dd7"
}
]
},
{
"Tag": "li",
"children": [
"客户端序号保护密钥(record number key): ",
{
"Tag": "code",
"content": "beed6218676635c2cb46a45694144fec"
}
]
},
{
"Tag": "li",
"children": [
"服务器端握手时密钥: ",
{
"Tag": "code",
"content": "004e03e64ab6cba6b542775ec230e20a"
}
]
},
{
"Tag": "li",
"children": [
"服务器端握手时向量: ",
{
"Tag": "code",
"content": "6d9924be044ee97c624913f2"
}
]
},
{
"Tag": "li",
"children": [
"服务器端序号保护密钥: ",
{
"Tag": "code",
"content": "7173fac51194e775001d625ef69d7c9f"
}
]
}
]
}
]
================================================
FILE: src/DTLS/clientHelloDatagram.json
================================================
[
"DTLS 加密会话以 \"ClientHello\" 开始。客户端提供的信息包括以下内容:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "客户端随机数(用于之后的握手过程)"
},
{
"Tag": "li",
"content": "一个客户端支持的有序的加解密算法数组"
},
{
"Tag": "li",
"content": "用于密钥交换的公钥"
},
{
"Tag": "li",
"content": "客户端支持的协议版本列表"
}
]
},
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"DTLS 数据头",
{
"props": {
"className": "bytes"
},
"content": "16 fe fd 00 00 00 00 00 00 00 00 00 9d"
},
[
"每个 DTLS 记录都以一个 type、一些序列信息(seq info)和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 表示 DTLS 记录类型 0x16(22, Handshake)"
},
{
"Tag": "li",
"content": "fe fd - 协议版本 (DTLS 1.2, 细节见下文)"
},
{
"Tag": "li",
"content": "00 00 - 密钥序列指示(key epoch,每次密钥更新时都会递增)"
},
{
"Tag": "li",
"content": "00 00 00 00 00 00 - DTLS 序列号 0x0(0)"
},
{
"Tag": "li",
"content": "00 9d - 紧接着的数组载荷长度 0x9D(157) 字节"
}
]
},
"DTLS 版本的编码方式是将协议版本分成几个部分,然后取每个部分的补码。(因此 \"1.3\" 变成 {1, 3},变成字节 0xFE 0xFC)。这种补码技术使 DTLS 版本与 TLS 版本有所差别。",
"由于已经创建和部署的网络中间件(middleboxes)不允许它们所不承认的协议版本通过,因此所有 DTLS 1.3 会话在未加密的记录中都会显示为 DTLS 1.2(0xFE 0xFD)。"
]
],
[
"TLS 握手记录头",
{
"props": {
"className": "bytes"
},
"content": "01 00 00 91"
},
[
"每个 TLS 握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "01 - 握手消息类型 0x01 (ClientHello)"
},
{
"Tag": "li",
"content": "00 00 91 - 紧接着的握手消息数据的长度 0x91 (145) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes"
},
"content": "00 00 00 00 00 00 00 91"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 00 - DTLS 序列号 0x0(0)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 00 91 - 表明之后的记录碎片的长度为 0x91(145) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"客户端版本(废弃)",
{
"props": {
"className": "bytes"
},
"content": "fe fd"
},
[
"DTLS 版本的编码方式是将协议版本分成几个部分,然后取每个部分的补码。(因此 \"1.3\" 变成 {1, 3},变成字节 0xFE 0xFC)。这种补码技术使 DTLS 版本与 TLS 版本有所差别。",
"由于已经创建和部署的网络中间件(middleboxes)不允许它们所不承认的协议版本通过,因此所有 DTLS 1.3 会话在未加密的记录中都会显示为 DTLS 1.2(0xFE 0xFD)。所有的 DTLS 1.3 及以上版本的会话需要通过后面提到的\"支持的版本\"拓展协商真实版本号。"
]
],
[
"客户端随机数",
{
"props": {
"className": "bytes"
},
"content": "e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff"
},
[
"客户端提供的 32 字节的随机数。这个数将在之后的会话中使用。在本文的例子中,我们暂时将一个方便记忆的字符串当作随机数。"
]
],
[
"会话 ID (废弃)",
{
"props": {
"className": "bytes"
},
"content": "00"
},
[
"这是一个废弃(legacy)字段,不在 DTLS 1.3 中使用。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 - 0 字节的会话 ID"
}
]
}
]
],
[
"Cookie (废弃)",
{
"props": {
"className": "bytes"
},
"content": "00"
},
[
"这是一个废弃(legacy)字段,不在 DTLS 1.3 中使用。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 - 0 字节的 Cookie"
}
]
}
]
],
[
"加解密算法列表",
{
"props": {
"className": "bytes"
},
"content": "00 06 13 01 13 02 13 03"
},
[
"客户端提供一个有序的列表,以说明它支持哪些加解密算法进行加密。该列表是按照客户端的偏好顺序排列的,以最高偏好为先。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 06 - 6 字节的加解密算法列表数据"
},
{
"Tag": "li",
"content": "13 01 - 代表 TLS_AES_128_GCM_SHA256"
},
{
"Tag": "li",
"content": "13 02 - 代表 TLS_AES_256_GCM_SHA384"
},
{
"Tag": "li",
"content": "13 03 - 代表 TLS_CHACHA20_POLY1305_SHA256"
}
]
},
{
"children": [
"完整加解密算法列表请查看 ",
{
"Tag": "a",
"props": {
"href": "https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml"
},
"content": "IANA tls-parameters"
},
" 的 TLS Cipher Suites 部分。"
]
}
]
],
[
"压缩算法",
{
"props": {
"className": "bytes"
},
"content": "01 00"
},
[
{
"children": [
"以前的 TLS 版本支持压缩(以及对应延伸出的 DTLS 版本),这被发现会泄露加密数据的信息(见 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/333314023"
},
"content": "CRIME/BREACH 攻击"
},
")。"
]
},
"TLS 1.3 (DTLS 1.3)不再允许压缩。所以这个字段不会变化:采用 \"null\" 压缩算法,对数据不做任何改变。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "01 - 1 字节的压缩算法长度"
},
{
"Tag": "li",
"content": "00 - 代表 \"没有\"(null) 压缩算法"
}
]
}
]
],
[
"扩展的长度",
{
"props": {
"className": "bytes"
},
"content": "00 61"
},
[
"客户端提供了一个可选的扩展列表,服务器可以根据它来采取某些行动或启用某些特性。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 61 - 扩展列表的长度为 0x61(97) 字节"
}
]
},
"每个扩展将以两个字节开始,表明它是哪个扩展。然后是两个字节的内容长度字段,最后是扩展的具体内容。"
]
],
[
"扩展 - 算法公钥列表(Key Share)",
{
"props": {
"className": "bytes"
},
"content": "00 33 00 26 00 24 00 1d 00 20 35 80 72 d6 36 58 80 d1 ae ea 32 9a df 91 21 38 38 51 ed 21 a2 8e 3b 75 e9 65 d0 d2 cd 16 62 54"
},
[
"客户端会给某些它认为服务器也会支持的算法发送对应的短暂的公钥。这允许在 ClientHello 和 ServerHello 消息之后的其余握手记录被加密。而不用像以前的协议版本,以透明的方式发送握手记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 33 - 表示这是 \"算法公钥列表\" 扩展"
},
{
"Tag": "li",
"content": "00 26 - 扩展信息长度为 0x26(38) 字节 "
},
{
"Tag": "li",
"content": "00 24 - 算法公钥列表长度为 0x24(36) 字节"
},
{
"Tag": "li",
"content": "00 1d - 代表 x25519 算法(例子中为通过 curve25519 算法进行密钥交换)"
},
{
"Tag": "li",
"content": "00 20 - 公钥长度为 0x20(32) 字节"
},
{
"Tag": "li",
"content": "35 80 ... 62 54 - \"客户端准备密钥交换\" 步骤中生成的公钥"
}
]
}
]
],
[
"扩展 - 支持的版本",
{
"props": {
"className": "bytes"
},
"content": "00 2b 00 03 02 fe fc"
},
[
"客户端表明其支持 DTLS 1.3。由于兼容性的原因,这被放在一个扩展中,而不是上面的客户端版本字段。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 2b - 表示这是 \"支持的版本\" 扩展"
},
{
"Tag": "li",
"content": "00 03 - 扩展信息长度为 0x03(3) 字节 "
},
{
"Tag": "li",
"content": "02 - DTLS 版本长度为 0x02(2) 字节"
},
{
"Tag": "li",
"content": "fe fc - 代表 DTLS 1.3"
}
]
}
]
],
[
"扩展 - 签名算法列表",
{
"props": {
"className": "bytes"
},
"content": "00 0d 00 20 00 1e 06 03 05 03 04 03 02 03 08 06 08 0b 08 05 08 0a 08 04 08 09 06 01 05 01 04 01 03 01 02 01"
},
[
"这个扩展表示客户端支持哪些签名算法。这可能会影响到服务器提交给客户端的证书,以及服务器在 CertificateVerify 记录中发送的签名。",
"这个列表同样是按照客户端的偏好降序排列的。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 0d - 表示这是 \"签名算法列表\" 扩展"
},
{
"Tag": "li",
"content": "00 20 - 扩展信息长度为 0x20(32) 字节 "
},
{
"Tag": "li",
"content": "00 1e - 算法列表长度为 0x1e(30) 字节"
},
{
"Tag": "li",
"content": "06 03 - 代表 ECDSA-SECP512r1-SHA512 算法"
},
{
"Tag": "li",
"content": "05 03 - 代表 ECDSA-SECP384r1-SHA384 算法"
},
{
"Tag": "li",
"content": "04 03 - 代表 ECDSA-SECP256r1-SHA256 算法"
},
{
"Tag": "li",
"content": "02 03 - 代表 ECDSA-SHA1 算法"
},
{
"Tag": "li",
"content": "08 06 - 代表 RSA-PSS-RSAE-SHA512 算法"
},
{
"Tag": "li",
"content": "08 0b - 代表 RSA-PSS-PSS-SHA512 算法"
},
{
"Tag": "li",
"content": "08 05 - 代表 RSA-PSS-RSAE-SHA384 算法"
},
{
"Tag": "li",
"content": "08 0a - 代表 RSA-PSS-PSS-SHA384 算法"
},
{
"Tag": "li",
"content": "08 04 - 代表 RSA-PSS-RSAE-SHA256 算法"
},
{
"Tag": "li",
"content": "08 09 - 代表 RSA-PSS-PSS-SHA256 算法"
},
{
"Tag": "li",
"content": "06 01 - 代表 RSA-PKCS1-SHA512 算法"
},
{
"Tag": "li",
"content": "05 01 - 代表 RSA-PKCS1-SHA384 算法"
},
{
"Tag": "li",
"content": "04 01 - 代表 RSA-PKCS1-SHA256 算法"
},
{
"Tag": "li",
"content": "03 01 - 代表 SHA224-RSA 算法"
},
{
"Tag": "li",
"content": "02 01 - 代表 RSA-PKCS1-SHA1 算法"
}
]
}
]
],
[
"扩展 - Encrypt-then-MAC",
{
"props": {
"className": "bytes"
},
"content": "00 16 00 00"
},
[
{
"children": [
"客户端表示其支持 EtM。EtM 可以防止早期版本的 TLS 和 DTLS 的",
{
"Tag": "a",
"props": {
"href": "https://iacr.org/archive/crypto2001/21390309.pdf"
},
"content": "某些漏洞"
},
"。在 DTLS 1.3 中,这个机制始终被开启,因此这个扩展在会话中没有影响。"
]
},
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 16 - 表示这是 \"EtM\" 扩展"
},
{
"Tag": "li",
"content": "00 00 - 扩展信息长度为 0x00(0) 字节 "
}
]
}
]
],
[
"扩展 - 支持的组",
{
"props": {
"className": "bytes"
},
"content": "00 0a 00 04 00 02 00 1d"
},
[
"(例子中的)客户端表示它支持一种类型的椭圆曲线算法(ECC)。为了使这个扩展更加通用,未来可以支持其他的密码学类型,因此称这些为 \"支持的组\" 而不是 \"支持的曲线\"。",
"这个列表同样是按照客户端的偏好降序排列的。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 0a - 表示这是 \"支持的组\" 扩展"
},
{
"Tag": "li",
"content": "00 04 - 扩展信息长度为 0x04(4) 字节 "
},
{
"Tag": "li",
"content": "00 02 - 曲线列表条目长度为 0x02(2) 字节"
},
{
"Tag": "li",
"content": "00 1d - 代表 x25519 曲线"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/clientKeyExchangeGeneration.json
================================================
[
"连接开始时,客户端生成一个用于密钥交换的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。",
{
"Tag": "p",
"children": [
"学习 DTLS 并不需要深入了解,但你可以从",
{
"Tag": "a",
"props": { "href": "https://cangsdarm.github.io/illustrate/x25519" },
"content": "X25519 密钥交换算法"
},
"获取涉及到的密钥交换算法的具体解释。"
]
},
[
"**私钥**是 0 到 ",
{
"Tag": "Math",
"content": "2^256-1"
},
" 之间的一个随机整数(32bytes, 256bits)",
"。为方便后续解释,假设我们生成的私钥是:"
],
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
}
]
},
"**公钥**由上面提到的 X25519 密钥交换算法生成。例子中计算出的公钥应如下所示:",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254"
}
]
},
"公钥的计算也可以在命令行中运行以下命令得到:",
{
"Tag": "CodeSample",
"props": {
"code": "The public key calculation can be confirmed at the command line:\n### requires openssl 1.1.0 or higher\n$ openssl pkey -noout -text < client-ephemeral-private.key\n\nX25519 Private-Key:\npriv:\n 20:21:22:23:24:25:26:27:28:29:2a:2b:2c:2d:2e:\n 2f:30:31:32:33:34:35:36:37:38:39:3a:3b:3c:3d:\n 3e:3f\npub:\n 35:80:72:d6:36:58:80:d1:ae:ea:32:9a:df:91:21:\n 38:38:51:ed:21:a2:8e:3b:75:e9:65:d0:d2:cd:16:\n 62:54"
}
},
"此时,网络连接仍在准备中,没有任何数据被传输。"
]
================================================
FILE: src/DTLS/index.js
================================================
const data = {
intro: {
title: "图解 DTLS 连接",
subtitle: "对每一个字节的解释和再现",
desc: 'DTLS 应被称为 "通过数据报传输的TLS";到目前为止,有五个 DTLS-over-XYZ 的 RFC,涵盖了 UDP、DCCP、CAPWAP、SCTP 和 SRTP',
intro:
'在这个演示中,客户端通过 DTLS 1.3 加密协商连接服务器。客户端发送"ping"、接收"pong"后终止连接。点击下面开始探索。',
},
sections: [
{
type: "RecOuter",
tags: ["calculation", "client"],
label: "客户端准备密钥交换",
illustration: {
src: "https://quic.xargs.org/images/key1.png",
width: "135",
height: "250",
},
json: () => import("./clientKeyExchangeGeneration.json"),
},
{
type: "RecOuter",
tags: ["record", "client"],
label: "ClientHello 数据报",
json: () => import("./clientHelloDatagram.json"),
},
{
type: "RecOuter",
tags: ["calculation", "server"],
label: "服务器端准备密钥交换",
illustration: {
src: "https://quic.xargs.org/images/key3.png",
width: "130",
height: "250",
},
json: () => import("./serverKeyExchangeGeneration.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "ServerHello 数据报",
json: () => import("./serverHelloDatagram.json"),
},
{
type: "RecOuter",
tags: ["calculation", "server"],
label: "服务器端生成握手密钥",
illustration: {
src: "https://quic.xargs.org/images/key5.png",
width: "124",
height: "250",
},
json: () => import("./serverHandshakeKeysCalc.json"),
},
{
type: "RecOuter",
tags: ["calculation", "client"],
label: "客户端生成握手密钥",
illustration: {
src: "https://quic.xargs.org/images/key6.png",
width: "105",
height: "250",
},
json: () => import("./clientHandshakeKeysCalc.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端加密后的额外扩展数据报",
illustration: {
src: "https://quic.xargs.org/images/key5.png",
width: "124",
height: "250",
},
json: () => import("./serverEncryptedExtensionsDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端证书数据报",
illustration: {
src: "https://quic.xargs.org/images/key3.png",
width: "130",
height: "250",
},
json: () => import("./serverCertificateDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端证书验证数据报",
illustration: {
src: "https://quic.xargs.org/images/key5.png",
width: "124",
height: "250",
},
json: () => import("./serverCertVerifyDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端握手完成数据报",
json: () => import("./serverHandshakeFinishedDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "client"],
label: "客户端握手完成数据报",
json: () => import("./clientHandshakeFinishedDatagram.json"),
},
{
type: "RecOuter",
tags: ["calculation", "server"],
label: "服务器端生成会话密钥",
illustration: {
src: "https://quic.xargs.org/images/key9.png",
width: "97",
height: "250",
},
json: () => import("./serverApplicationKeysCalc.json"),
},
{
type: "RecOuter",
tags: ["calculation", "client"],
label: "客户端生成会话密钥",
illustration: {
src: "https://quic.xargs.org/images/key8.png",
width: "97",
height: "250",
},
json: () => import("./clientApplicationKeysCalc.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端握手 ACK 数据报",
id: "serverHandshake3",
json: () => import("./serverACKDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "client"],
label: "客户端会话数据报",
json: () => import("./clientApplicationDataDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端会话数据报",
json: () => import("./serverApplicationDataDatagram.json"),
},
{
type: "RecOuter",
tags: ["record", "server"],
label: "服务器端警告数据报(alert datagram)",
json: () => import("./serverAlertDatagram.json"),
},
],
ending: {
mother: "https://dtls.xargs.org/",
desc: "你可能也对 <a href='/illustrate/tls13' target='_blank'>TLS 1.3</a> 的内容感兴趣。",
},
};
export default data;
================================================
FILE: src/DTLS/serverACKDatagram.json
================================================
[
"每个对等端(peer)必须响应或确认从其他对等端收到的数据,否则对应对等端将假定数据已丢失并会再次发送。",
"因此,在这个记录中,服务器端需要确认收到了客户端握手完成的记录。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2f"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2f 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"11",
"加密序列指示(Encryption epoch 3),现在密钥是会话时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "31 50"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 00"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from application keys calc step above\n$ key=57ba02596c6a1352d7fe8416c7e17d5a\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=ea80ab8e08c93895418d243571ea6de7\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\n3150\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 23"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 DTLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 23 - 表示 DTLS 记录长度为 0x23(35) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "ea 80 ab 8e 08 c9 38 95 41 8d 24 35 71 ea 6d e7 d8 63 ee"
},
["这些数据使用服务器端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "84 23 0b b6 04 3c b3 84 df 94 b6 da 28 5a 3b c4"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端会话密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Application Keys Calc\" step\n$ key=2b65fffbbc8189474aa2003c43c32d4d\n$ iv=582f5a11bdaf973fe3ffeb4e\n### from this record\n$ recdata=2f00000023\n$ authtag=84230bb6043cb384df94b6da285a3bc4\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 00 10 00 00 00 00 00 00 00 02 00 00 00 00 00 00 |................|\n00000010 00 00 1a |...|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"ACK 长度",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 10"
},
[
"每个 ACK 消息都以一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 10 - 紧接着的 ACK 消息数据的长度 0x10 (16) 字节"
}
]
}
]
],
[
"已收到记录信息(Record Acknowledgement)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00"
},
[
"服务器确认其收到的记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 00 00 00 00 00 02 - 记录的序列指示号(record epoch),握手密钥"
},
{
"Tag": "li",
"content": "00 00 00 00 00 00 00 00 - 对应记录序号"
}
]
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "1a"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "1a - 类型为 0x1a(26), ACK 记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverAlertDatagram.json
================================================
[
"服务器端发送警告信号,表明连接即将终止(此时是有序终止即正常终止连接)",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2f"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2f 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"11",
"加密序列指示(Encryption epoch 3),现在密钥是会话时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "69 0c"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 02"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from application keys calc step above\n$ key=57ba02596c6a1352d7fe8416c7e17d5a\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=dd8cd07daa964fd1ab508825378fc96f\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\n690e\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 13"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 DTLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 13 - 表示 DTLS 记录长度为 0x13(19) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "dd 8c d0"
},
["这些数据使用服务器端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "7d aa 96 4f d1 ab 50 88 25 37 8f c9 6f a8 b1 e8"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端会话密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 1。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Application Keys Calc\" step\n$ key=2b65fffbbc8189474aa2003c43c32d4d\n$ iv=582f5a11bdaf973fe3ffeb4e\n### from this record\n$ recdata=2f00020013\n$ authtag=7daa964fd1ab508825378fc96fa8b1e8\n$ recordnum=2\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 01 00 15"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"警告信息",
{
"props": {
"className": "bytes encrypted"
},
"content": "01 00"
},
[
"服务器发送一个 \"连接关闭通知\"(close notify) 的警告,表示连接正有序地终止(正常终止)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "01 - 警告级别为 0x01(warning), 未使用"
},
{
"Tag": "li",
"content": "00 - 表明这是 close notify 警告"
}
]
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "15"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "15 - 类型为 0x15(21), 警告"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverApplicationDataDatagram.json
================================================
[
"服务器端响应数据:字符串\"pong\"",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2f"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2f 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"11",
"加密序列指示(Encryption epoch 3),现在密钥是会话时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "a2 58"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 01"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from application keys calc step above\n$ key=57ba02596c6a1352d7fe8416c7e17d5a\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=f5bd33f27b72780e351fa00703fb9f65\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\na259\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 15"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 DTLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 15 - 表示 DTLS 记录长度为 0x15(21) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "f5 bd 33 f2 7b"
},
["这些数据使用服务器端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "72 78 0e 35 1f a0 07 03 fb 9f 65 8c 68 9f 95 ae"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端会话密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 1。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Application Keys Calc\" step\n$ key=2b65fffbbc8189474aa2003c43c32d4d\n$ iv=582f5a11bdaf973fe3ffeb4e\n### from this record\n$ recdata=2f00010015\n$ authtag=72780e351fa00703fb9f658c689f95ae\n$ recordnum=1\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 70 6f 6e 67 17 |pong.|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据",
{
"props": {
"className": "bytes encrypted"
},
"content": "70 6f 6e 67"
},
["字符串\"pong\""]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "17"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "17 - 类型为 0x17(23), 会话数据"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverApplicationKeysCalc.json
================================================
[
"服务器端现在可以计算应用会话时的密钥了。在这个计算中,服务器使用了以下信息:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "服务器端密文(之前准备握手密钥时生成)"
},
{
"Tag": "li",
"content": "ClientHello 到 服务器握手结束 所有握手时包的 SHA256 哈希值(哈希值的输入不包括明文的记录头部、DTLS 特有的记录头以及最后 1 字节的记录类型)"
}
]
},
"32 字节的 \"handshake_hash\" 最终值是:",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b"
}
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ (\n cat record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';\n cat record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';\n cat record-encext | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cert | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-sfin | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n )| openssl sha256\n\n77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b"
}
},
"然后,我们将哈希值和共享的签名信息进行一些密钥派生操作(key derivation operations),以防止可能的攻击:",
{
"Tag": "pre",
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "empty_hash = SHA256(\"\")\nderived_secret = HKDF-Expand-Label(key: handshake_secret, label: \"derived\", ctx: empty_hash, len: 32)\nmaster_secret = HKDF-Extract(salt: derived_secret, key: 00...)\nclient_secret = HKDF-Expand-Label(key: master_secret, label: \"c ap traffic\", ctx: handshake_hash, len: 32)\nserver_secret = HKDF-Expand-Label(key: master_secret, label: \"s ap traffic\", ctx: handshake_hash, len: 32)\nclient_application_key = HKDF-Expand-Label(key: client_secret, label: \"key\", ctx: \"\", len: 16)\nserver_application_key = HKDF-Expand-Label(key: server_secret, label: \"key\", ctx: \"\", len: 16)\nclient_application_iv = HKDF-Expand-Label(key: client_secret, label: \"iv\", ctx: \"\", len: 12)\nserver_application_iv = HKDF-Expand-Label(key: server_secret, label: \"iv\", ctx: \"\", len: 12)"
}
]
},
{
"children": [
"在命令行中使用",
{
"Tag": "a",
"props": { "href": "https://dtls.xargs.org/files/hkdf-dtls.sh" },
"content": "原作者制作的 HKDF 命令行脚本"
},
",你也可以自己试试:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ handshake_hash=77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b\n$ handshake_secret=d0d1397bb3c445d37f26f7ed00c83b73d2f67540de3761465ffe524f8f944e12\n$ zero_key=0000000000000000000000000000000000000000000000000000000000000000\n$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')\n$ derived_secret=$(./hkdf-dtls expandlabel $handshake_secret \"derived\" $empty_hash 32)\n$ master_secret=$(./hkdf-dtls extract $derived_secret $zero_key)\n$ csecret=$(./hkdf-dtls expandlabel $master_secret \"c ap traffic\" $handshake_hash 32)\n$ ssecret=$(./hkdf-dtls expandlabel $master_secret \"s ap traffic\" $handshake_hash 32)\n$ client_application_key=$(./hkdf-dtls expandlabel $csecret \"key\" \"\" 16)\n$ server_application_key=$(./hkdf-dtls expandlabel $ssecret \"key\" \"\" 16)\n$ client_application_iv=$(./hkdf-dtls expandlabel $csecret \"iv\" \"\" 12)\n$ server_application_iv=$(./hkdf-dtls expandlabel $ssecret \"iv\" \"\" 12)\n$ client_sn_key=$(./hkdf-dtls expandlabel $csecret \"sn\" \"\" 16)\n$ server_sn_key=$(./hkdf-dtls expandlabel $ssecret \"sn\" \"\" 16)\n$ echo client_key: $client_application_key\n$ echo client_iv: $client_application_iv\n$ echo server_key: $server_application_key\n$ echo server_iv: $server_application_iv\n$ echo client_sn_key: $client_sn_key\n$ echo server_sn_key: $server_sn_key\n\nclient_key: 9ba90dbce8857bc1fcb81d41a0465cfe\nclient_iv: 682219974631fa0656ee4eff\nserver_key: 2b65fffbbc8189474aa2003c43c32d4d\nserver_iv: 582f5a11bdaf973fe3ffeb4e\nclient_sn_key: 5cb5bd8bac29777c650c0dde22d16d47\nserver_sn_key: 57ba02596c6a1352d7fe8416c7e17d5a"
}
},
"由此我们可以得到以下密钥以及向量:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"children": [
"客户端会话密钥: ",
{
"Tag": "code",
"content": "9ba90dbce8857bc1fcb81d41a0465cfe"
}
]
},
{
"Tag": "li",
"children": [
"客户端会话向量: ",
{
"Tag": "code",
"content": "682219974631fa0656ee4eff"
}
]
},
{
"Tag": "li",
"children": [
"客户端序号保护密钥: ",
{
"Tag": "code",
"content": "5cb5bd8bac29777c650c0dde22d16d47"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话密钥: ",
{
"Tag": "code",
"content": "2b65fffbbc8189474aa2003c43c32d4d"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话向量: ",
{
"Tag": "code",
"content": "582f5a11bdaf973fe3ffeb4e"
}
]
},
{
"Tag": "li",
"children": [
"服务器端序号保护密钥: ",
{
"Tag": "code",
"content": "57ba02596c6a1352d7fe8416c7e17d5a"
}
]
}
]
}
]
================================================
FILE: src/DTLS/serverCertVerifyDatagram.json
================================================
[
"服务器提供证书验证相关的数据,用以验证服务器密钥交换生成过程中产生的短暂公钥与证书私钥的所有权是否一致。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2e"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2e 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"10",
"加密序列指示(Encryption epoch 2),现在密钥是握手时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "a4 3e"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 02"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from handshake keys calc step above\n$ key=7173fac51194e775001d625ef69d7c9f\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=83bedfea0f4aa578453af4f4a4be4106\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\na43c\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "01 24"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 TLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "01 21 - 表示 TLS 记录长度为 0x121(289) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手密钥\"加密"
},
"content": "83 be df ea 0f 4a a5 78 45 3a f4 f4 a4 be 41 06 9b eb e5 9c e4 93 3b f2 f2 ff 35 36 f0 e6 11 45 9f 7a fc 07 14 1e 4a 80 e4 b1 10 f2 c5 48 24 4e 83 42 cd 13 46 26 f0 d6 bc 12 2c 6e e3 cc 81 64 e3 e1 1f b8 bc 7b 58 ff 8d ef af 99 c9 26 81 f7 42 64 cc 29 5d f2 69 b4 63 af e5 78 53 ba 86 04 bd 8e ef 74 91 a0 fc 5a 5d df c2 2b 87 f7 cc 55 94 fd 2b 13 69 68 ab 07 ce 1d 84 33 07 df 9f 41 37 27 11 0f e0 5a c6 df 33 7c 44 4c 9a 2d 8b 28 30 b3 50 48 13 72 dd a1 4b e3 04 63 cb 94 16 f8 15 b7 29 b8 20 be b9 1e df 34 f8 b2 29 fa 71 4d fa 58 68 61 c5 25 15 aa d2 8e 98 52 90 d2 a7 e1 97 df 5a 4f 73 20 4d 95 2c a3 e2 34 af 34 fa e6 5a 3a 34 c1 33 8b 52 dd b7 8e 87 a9 14 95 21 2c 8e da ed 59 6e 0b 4b ad 18 65 66 8d 5a 33 9f d7 61 31 43 bc b8 5d 96 10 41 22 f6 17 e5 39 3b 4c ba 44 d0 86 e5 32 c7 39 e8 15 ea dc 2a 84 07 c4 72"
},
["这些数据使用服务器端的\"握手密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "bd f0 f6 f0 06 0d b4 71 19 71 38 7c 21 89 39 4f"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端握手密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 2。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Handshake Keys Calc\" step\n$ key=004e03e64ab6cba6b542775ec230e20a\n$ iv=6d9924be044ee97c624913f2\n### from this record\n$ recdata=2e00020121\n$ authtag=bdf0f6f0060db4711971387c2189394f\n$ recordnum=2\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 0f 00 01 04 00 03 00 00 00 00 01 04 08 04 01 00 |................|\n00000010 2c 76 3d 6a d3 d8 af 7f a3 7d a6 d8 d9 0e 73 7c |,v=j?د.?}???.s||\n00000020 ea 53 ee 7a ff a5 61 48 74 cc 68 48 9c 73 a2 f3 |?S?z??aHt?hH.s??|\n00000030 a0 43 cb ba e6 c2 7a 41 91 0e de 9a df c7 22 23 |?C˺??zA..?.??\"#|\n00000040 58 26 12 ec 96 79 fe 1f 9f a5 f4 a4 b6 12 f8 6f |X&.?.y?..????.?o|\n... snip ..."
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"握手消息头",
{
"props": {
"className": "bytes encrypted"
},
"content": "0f 00 01 04"
},
[
"每个握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "0f - 握手消息类型 0x0f (certificate verify)"
},
{
"Tag": "li",
"content": "00 01 04 - 紧接着的握手消息数据的长度 0x104 (260) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 03 00 00 00 00 01 04"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 03 - DTLS 序列号 0x3(3)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 01 04 - 表明之后的记录碎片的长度为 0x104(260) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"签名",
{
"props": {
"className": "bytes encrypted"
},
"content": "08 04 01 00 2c 76 3d 6a d3 d8 af 7f a3 7d a6 d8 d9 0e 73 7c ea 53 ee 7a ff a5 61 48 74 cc 68 48 9c 73 a2 f3 a0 43 cb ba e6 c2 7a 41 91 0e de 9a df c7 22 23 58 26 12 ec 96 79 fe 1f 9f a5 f4 a4 b6 12 f8 6f 40 88 49 a3 29 f7 63 e0 4f be 95 9a 91 e8 d1 8d 4a ba 79 29 57 6f a0 24 ec b2 37 d6 33 78 e9 8e e5 9d c9 59 49 b2 63 b3 06 53 0a 2e 6f b9 b2 2f a2 3c 64 32 33 43 03 89 33 01 fd 60 e2 05 82 6e b9 ec 41 4f ec 5f 9a 0d 6f 8f 3d 89 a0 9f 14 8e 0f 05 03 49 bc 1e 17 97 d9 28 1e ed f6 e7 66 9c e2 56 ae 79 d4 ee 8c 96 56 0d cf 07 6c 2a 45 a4 ee e8 d2 79 71 0f 0c e7 03 4a 3f 5c aa 94 41 4e ae df 61 08 48 66 e4 9e 81 88 3e e2 1a 12 59 3c cb 96 dd 11 76 9e 34 0f 1e 6c c2 14 b0 57 95 e5 4a fc 94 79 84 5e 4d f2 bf 96 9f bb 21 8c b9 c4 b8 34 a8 51 be 34 75 a1 45 2f 4b 33 55 4f 9d 65"
},
[
"由于服务器会为每个会话都生成短暂的密钥,所以和 TLS 之前的版本不同,密钥与证书不会有内在的联系。",
"为了证明服务器拥有服务器证书(在这个 TLS 会话中的有效性),它需要使用证书的私钥对握手信息的哈希进行签名。而客户端可以通过使用证书的公钥来证明该签名的有效性。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "08 04 - 表明签名算法为 RSA-PSS-RSAE-SHA256"
},
{
"Tag": "li",
"content": "01 00 - 表明签名算法长度为 0x100(256) 字节"
},
{
"Tag": "li",
"content": "2c 76 3d ... 4f 9d 65 - 签名"
}
]
},
"签名过程不能在命令行中逐字逐句地复制,因为签名工具在签名中引入了随机或变化的数据。",
{
"children": [
"相反,我们可以像客户端那样,在命令行使用",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/server.crt"
},
"content": "服务器的证书"
},
"提供的公钥来验证签名:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### find the hash of the conversation to this point, excluding\n### cleartext record headers, DTLS-only record headers,\n### or 1-byte decrypted record trailers\n$ handshake_hash=$((\n cat record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-encext | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cert | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n )| openssl sha256)\n\n### build the data that was signed:\n### 1. add 64 space characters\n$ echo -n ' ' > /tmp/tosign\n$ echo -n ' ' >> /tmp/tosign\n### 2. add this fixed string\n$ echo -n 'TLS 1.3, server CertificateVerify' >> /tmp/tosign\n### 3. add a single null character\n$ echo -en '\\u0000' >> /tmp/tosign\n### 4. add hash of handshake to this point\n$ echo $handshake_hash | xxd -r -p >> /tmp/tosign\n\n### copy the signature that we want to verify\n$ echo \"2c 76 3d 6a d3 d8 af 7f a3 7d a6 d8 d9 0e 73 7c ea 53 ee 7a\n ff a5 61 48 74 cc 68 48 9c 73 a2 f3 a0 43 cb ba e6 c2 7a 41 91 0e\n de 9a df c7 22 23 58 26 12 ec 96 79 fe 1f 9f a5 f4 a4 b6 12 f8 6f\n 40 88 49 a3 29 f7 63 e0 4f be 95 9a 91 e8 d1 8d 4a ba 79 29 57 6f\n a0 24 ec b2 37 d6 33 78 e9 8e e5 9d c9 59 49 b2 63 b3 06 53 0a 2e\n 6f b9 b2 2f a2 3c 64 32 33 43 03 89 33 01 fd 60 e2 05 82 6e b9 ec\n 41 4f ec 5f 9a 0d 6f 8f 3d 89 a0 9f 14 8e 0f 05 03 49 bc 1e 17 97\n d9 28 1e ed f6 e7 66 9c e2 56 ae 79 d4 ee 8c 96 56 0d cf 07 6c 2a\n 45 a4 ee e8 d2 79 71 0f 0c e7 03 4a 3f 5c aa 94 41 4e ae df 61 08\n 48 66 e4 9e 81 88 3e e2 1a 12 59 3c cb 96 dd 11 76 9e 34 0f 1e 6c\n c2 14 b0 57 95 e5 4a fc 94 79 84 5e 4d f2 bf 96 9f bb 21 8c b9 c4\n b8 34 a8 51 be 34 75 a1 45 2f 4b 33 55 4f 9d 65\" | xxd -r -p > /tmp/sig\n\n### extract the public key from the certificate\n$ openssl x509 -pubkey -noout -in server.crt > server.pub\n\n### verify the signature\n$ cat /tmp/tosign | openssl dgst -verify server.pub -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 -signature /tmp/sig\n\nVerified OK"
}
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "16"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 类型为 0x16(22), 握手记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverCertificateDatagram.json
================================================
[
"服务器会发送一个或多个证书:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "该主机的证书,包含主机名、公钥和第三方的签名(签名证明该证书主机名的所有者持有该证书的私钥)。"
},
{
"Tag": "li",
"content": "其他证书的可选列表,从主机证书一直到预先安装在客户端上的可信证书。其中每一个都对前一个证书进行签名,形成一个信任链。"
}
]
},
{
"children": [
"为了让这个例子不至于太过于庞大,我们只发送一个主机证书。证书以一种叫做 ",
{
"Tag": "a",
"props": {
"href": "https://www.cnblogs.com/flydean/p/16541577.html"
},
"content": "DER"
},
" 的二进制格式书写。"
]
},
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2e"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2e 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"10",
"加密序列指示(Encryption epoch 2),现在密钥是握手时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "ed 2b"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 01"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from handshake keys calc step above\n$ key=7173fac51194e775001d625ef69d7c9f\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=d3777e1adf9e98c8c4ffa072c2c3b6bb\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\ned2a\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "03 4b"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 TLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "03 4b - 表示 TLS 记录长度为 0x34B(843) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手密钥\"加密"
},
"content": "d3 77 7e 1a df 9e 98 c8 c4 ff a0 72 c2 c3 b6 bb cd 9f bd 2d 1f 34 3c 5d f9 54 d9 97 a2 cd 1d 33 42 a3 3d 3f 6a 85 e1 21 42 3c e0 02 ea 33 5e 37 7e 7a 21 5b 8a 9e cc 5e 26 7c 60 a2 bc 79 4e d1 d8 1f 39 8b ce df a3 68 fb db 7c a3 67 a0 46 65 5d 61 e4 86 67 62 fa ac fd a4 9d 0f 3a 39 71 86 d8 32 e4 81 87 d0 76 ea 8d e5 32 12 87 be 9b fd a2 15 19 da 58 e0 c4 80 56 99 7e 49 2e df e4 76 6b 2c d5 1e a1 2b c2 f6 d5 50 5b 80 e5 1a 64 5d a9 b0 7f bf 7a 01 b8 4d 5b a7 22 b2 e1 7d d9 52 8c 28 63 cd 63 a7 35 b5 4c d8 23 95 87 84 1a 59 2f be 57 5b 2d e1 8a 6c 99 f7 82 a9 56 e2 8c e7 69 67 42 67 3d 7e e7 37 f4 6e 9c ba a2 89 2d 97 21 ef cc c9 1f 16 72 26 a5 be 4c 9c d8 6b 97 fe f3 32 3f d1 92 f4 60 e8 ef 8b 91 3b bf 9f 97 05 63 85 d4 c3 ec 2b 2b dc 2e c4 8a 66 8c f6 f1 0d b3 fe 00 91 97 fa b9 8d 7c 2a 88 15 ac 5a 4e d3 aa 08 94 b9 f9 f9 95 12 43 0d f2 1f 13 4c 49 34 40 73 f9 af 32 8e 35 c2 e1 6b 91 3f 4e 61 33 21 e4 a7 9b d2 d3 38 47 32 1e 61 5d 58 94 09 b1 65 f9 c2 b0 18 80 4f 3c 33 40 e4 0a d5 f5 9a 26 46 0a 12 0f 2d 55 fc 8b ca 47 22 74 fd b9 06 09 a2 18 70 e1 cc 41 aa d0 24 fa 48 a8 6f 07 8f 90 8b c6 26 18 c4 c3 2f 0c fc fb b5 95 a7 d2 93 f4 ba ab 93 ff 35 f0 de 10 71 17 1e 4c 51 0d 75 dd 29 f5 0d 3d e8 1c ae 9e 1c 56 ed 60 9c 1b c7 27 5e ac 1d 69 33 df 08 93 dd 0e 3c 5c 7f d3 65 14 26 b3 e4 c3 ca 6d 46 1d 82 0a df ff 75 fb 7b 15 8b e9 89 30 89 da c9 30 a0 15 f8 9c b4 ef 22 7a b9 e4 3d f0 14 7a 25 07 59 e3 e0 1b 5d b7 48 0c 52 7a 1d 4b 8a 09 c4 ac 05 fc c6 d6 40 15 d6 af 2c 3e 52 15 03 a8 2f b9 02 5c 61 98 18 ca 31 fb 24 03 63 0a c0 6a b7 11 90 53 a7 02 86 24 0b 3f 8e 43 96 61 ad 95 48 7a a5 72 d7 08 60 8d d0 d4 fe 27 bb cf 1e df 50 3a 54 05 46 0b 9e 10 f6 93 4a 41 a8 cf b7 0b 60 90 6f 7e 66 d6 53 15 61 ef 08 ad e3 de 45 77 a7 77 6b f6 56 bb 48 5c ee 28 2c 83 7a a8 bc e0 6a e6 06 a1 71 d7 54 96 36 fe d8 3e 24 bf 9f 10 5b 7d 1d 02 da 30 86 ce 24 49 af a2 d0 ec 26 18 5d 0c 1f 05 2f 88 cd 9d 55 eb 12 4b da e3 66 7f 59 79 97 95 f9 27 50 b9 ca 70 55 66 86 6a 99 24 a2 46 a4 71 90 4b 2d 69 dc 17 cb fe 50 a5 62 ff 26 ff 9e 40 4d 7b 2a 11 67 0c 27 56 3f 3e 37 99 3c c6 e6 73 43 6d c3 a8 51 21 4d 6d 27 86 2b 64 5d cb 0b f4 d4 c7 44 0f 6a d4 83 ef 9d 58 fa b4 7d 24 4b d6 cf a6 8f 12 e9 aa ae cd 2d 52 8e 85 66 f9 7f 50 56 cf 8e fc 7d 1e 55 fb ee 1b e8 7f 7f 89 73 7c 8a fa 20 e4 96 37 0d 25 f7 52 99 e5 91 8c b9 4b a5 b5 ef db 84 7d 9c a5 44 a5 38 65 a3 6d 69 1e be 8b e8 e2 da 08 c1 7b e9 02 38 0d b9 a3 d7 04 91 b8 98 f8 c5 88 e7 44 64 8e b9 37 70 53 0c 83 ce cf"
},
["这些数据使用服务器端的\"握手密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "a4 30 70 21 45 22 93 8c 0e 66 82 9e f1 33 34 9b"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端握手密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 1。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Handshake Keys Calc\" step\n$ key=004e03e64ab6cba6b542775ec230e20a\n$ iv=6d9924be044ee97c624913f2\n### from this record\n$ recdata=2e0001034b\n$ authtag=a43070214522938c0e66829ef133349b\n$ recordnum=1\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 0b 00 03 2e 00 02 00 00 00 00 03 2e 00 00 03 2a |...............*|\n00000010 00 03 25 30 82 03 21 30 82 02 09 a0 03 02 01 02 |..a4..!0...?....|\n00000020 02 08 15 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 |...Z.??...0...*.|\n00000030 48 86 f7 0d 01 01 0b 05 00 30 22 31 0b 30 09 06 |H.?......0\"1.0..|\n00000040 03 55 04 06 13 02 55 53 31 13 30 11 06 03 55 04 |.U....US1.0...U.|\n00000050 0a 13 0a 45 78 61 6d 70 6c 65 20 43 41 30 1e 17 |...Example CA0..|\n00000060 0d 31 38 31 30 30 35 30 31 33 38 31 37 5a 17 0d |.181005013817Z..|\n00000070 31 39 31 30 30 35 30 31 33 38 31 37 5a 30 2b 31 |191005013817Z0+1|\n00000080 0b 30 09 06 03 55 04 06 13 02 55 53 31 1c 30 1a |.0...U....US1.0.|\n00000090 06 03 55 04 03 13 13 65 78 61 6d 70 6c 65 2e 75 |..U....example.u|\n... snip ...\n"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"握手消息头",
{
"props": {
"className": "bytes encrypted"
},
"content": "0b 00 03 2e"
},
[
"每个握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "0b - 握手消息类型 0x0b (certificate)"
},
{
"Tag": "li",
"content": "00 03 2e - 紧接着的握手消息数据的长度 0x32E (814) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 02 00 00 00 00 03 2e"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 02 - DTLS 序列号 0x2(2)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 03 2e - 表明之后的记录碎片的长度为 0x32E(814) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"请求上下文",
{
"props": {
"className": "bytes encrypted"
},
"content": "00"
},
[
"数据为空,因为该证书不是响应证书请求而发送。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 - 没有数据"
}
]
}
]
],
[
"所有证书长度",
{
"props": {
"className": "bytes"
},
"content": "00 03 2a"
},
[
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 03 2a - 紧接着的证书的长度 0x32A (810) 字节"
}
]
}
]
],
[
"第一个证书长度",
{
"props": {
"className": "bytes"
},
"content": "00 03 25"
},
[
"此时同时也是唯一一个",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 03 25 - 紧接着的证书的长度 0x325 (805) 字节"
}
]
}
]
],
[
"证书",
{
"props": {
"className": "bytes encrypted"
},
"content": "30 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 30 22 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 13 30 11 06 03 55 04 0a 13 0a 45 78 61 6d 70 6c 65 20 43 41 30 1e 17 0d 31 38 31 30 30 35 30 31 33 38 31 37 5a 17 0d 31 39 31 30 30 35 30 31 33 38 31 37 5a 30 2b 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 1c 30 1a 06 03 55 04 03 13 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 30 82 01 22 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 82 01 0f 00 30 82 01 0a 02 82 01 01 00 c4 80 36 06 ba e7 47 6b 08 94 04 ec a7 b6 91 04 3f f7 92 bc 19 ee fb 7d 74 d7 a8 0d 00 1e 7b 4b 3a 4a e6 0f e8 c0 71 fc 73 e7 02 4c 0d bc f4 bd d1 1d 39 6b ba 70 46 4a 13 e9 4a f8 3d f3 e1 09 59 54 7b c9 55 fb 41 2d a3 76 52 11 e1 f3 dc 77 6c aa 53 37 6e ca 3a ec be c3 aa b7 3b 31 d5 6c b6 52 9c 80 98 bc c9 e0 28 18 e2 0b f7 f8 a0 3a fd 17 04 50 9e ce 79 bd 9f 39 f1 ea 69 ec 47 97 2e 83 0f b5 ca 95 de 95 a1 e6 04 22 d5 ee be 52 79 54 a1 e7 bf 8a 86 f6 46 6d 0d 9f 16 95 1a 4c f7 a0 46 92 59 5c 13 52 f2 54 9e 5a fb 4e bf d7 7a 37 95 01 44 e4 c0 26 87 4c 65 3e 40 7d 7d 23 07 44 01 f4 84 ff d0 8f 7a 1f a0 52 10 d1 f4 f0 d5 ce 79 70 29 32 e2 ca be 70 1f df ad 6b 4b b7 11 01 f4 4b ad 66 6a 11 13 0f e2 ee 82 9e 4d 02 9d c9 1c dd 67 16 db b9 06 18 86 ed c1 ba 94 21 02 03 01 00 01 a3 52 30 50 30 0e 06 03 55 1d 0f 01 01 ff 04 04 03 02 05 a0 30 1d 06 03 55 1d 25 04 16 30 14 06 08 2b 06 01 05 05 07 03 02 06 08 2b 06 01 05 05 07 03 01 30 1f 06 03 55 1d 23 04 18 30 16 80 14 89 4f de 5b cc 69 e2 52 cf 3e a3 00 df b1 97 b8 1d e1 c1 46 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 03 82 01 01 00 59 16 45 a6 9a 2e 37 79 e4 f6 dd 27 1a ba 1c 0b fd 6c d7 55 99 b5 e7 c3 6e 53 3e ff 36 59 08 43 24 c9 e7 a5 04 07 9d 39 e0 d4 29 87 ff e3 eb dd 09 c1 cf 1d 91 44 55 87 0b 57 1d d1 9b df 1d 24 f8 bb 9a 11 fe 80 fd 59 2b a0 39 8c de 11 e2 65 1e 61 8c e5 98 fa 96 e5 37 2e ef 3d 24 8a fd e1 74 63 eb bf ab b8 e4 d1 ab 50 2a 54 ec 00 64 e9 2f 78 19 66 0d 3f 27 cf 20 9e 66 7f ce 5a e2 e4 ac 99 c7 c9 38 18 f8 b2 51 07 22 df ed 97 f3 2e 3e 93 49 d4 c6 6c 9e a6 39 6d 74 44 62 a0 6b 42 c6 d5 ba 68 8e ac 3a 01 7b dd fc 8e 2c fc ad 27 cb 69 d3 cc dc a2 80 41 44 65 d3 ae 34 8c e0 f3 4a b2 fb 9c 61 83 71 31 2b 19 10 41 64 1c 23 7f 11 a5 d6 5c 84 4f 04 04 84 99 38 71 2b 95 9e d6 85 bc 5c 5d d6 45 ed 19 90 94 73 40 29 26 dc b4 0e 34 69 a1 59 41 e8 e2 cc a8 4b b6 08 46 36 a0"
},
[
{
"children": [
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/server.crt"
},
"content": "该证书"
},
"采用 ASN.1 DER 编码。可以在命令行中使用以下命令转换为二进制数据:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ openssl x509 -outform der < server.crt | hexdump\n\n0000000 30 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15\n0000010 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7\n... snip ..."
}
}
]
],
[
"证书的扩展",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 00"
},
[
"服务器可以提供证书需要的扩展信息。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 00 - 没有扩展信息"
}
]
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "16"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 类型为 0x16(22), 握手记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverEncryptedExtensionsDatagram.json
================================================
[
"连接(包括握手)的数据从这时候起就能够被加密了。加密握手数据是 DTLS 1.3 的新特性。",
"任何不需要协商其他加密密钥的扩展都会列在这里。加密以隐藏它们不被窃听者和中间件(middleboxes)发现。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2e"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2e 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"10",
"加密序列指示(Encryption epoch 2),现在密钥是握手时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "79 fa"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 00"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from handshake keys calc step above\n$ key=7173fac51194e775001d625ef69d7c9f\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=ee9dcff3f8679a4859fe68377fb34ada\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\n79fa\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 2f"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 TLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 2f - 表示 TLS 记录长度为 0x2F(47) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手密钥\"加密"
},
"content": "ee 9d cf f3 f8 67 9a 48 59 fe 68 37 7f b3 4a da 85 df 87 9c 67 3e 50 1d 7a 4e 8f 19 50 e0 fc"
},
["这些数据使用服务器端的\"握手密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "f6 7f e4 42 e7 d7 d2 b8 a3 d5 fa 59 57 4f fd 00"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端握手密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Handshake Keys Calc\" step\n$ key=004e03e64ab6cba6b542775ec230e20a\n$ iv=6d9924be044ee97c624913f2\n### from this record\n$ recdata=2e0000002f\n$ authtag=f67fe442e7d7d2b8a3d5fa59574ffd00\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 08 00 00 12 00 01 00 00 00 00 00 12 00 10 00 0a |................|\n00000010 00 0c 00 0a 00 17 00 1d 00 18 00 19 01 00 16 |...............|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"握手消息头",
{
"props": {
"className": "bytes encrypted"
},
"content": "08 00 00 12"
},
[
"每个握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "08 - 握手消息类型 0x08 (encrypted extensions)"
},
{
"Tag": "li",
"content": "00 00 12 - 紧接着的握手消息数据的长度 0x12 (18) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 01 00 00 00 00 00 12"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 01 - DTLS 序列号 0x1(1)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 00 12 - 表明之后的记录碎片的长度为 0x12(18) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"扩展的长度",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 10"
},
[
"服务器向客户端返回的扩展有序列表的长度。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 10 - 扩展列表的长度为 0x10(16) 字节"
}
]
}
]
],
[
"扩展 - 支持的组",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 0a 00 0c 00 0a 00 17 00 1d 00 18 00 19 01 00"
},
[
"服务器返回它所支持的椭圆曲线加解密算法列表。为了使这个扩展更加通用,未来可以支持其他的密码学类型,因此称这些为 \"支持的组\" 而不是 \"支持的曲线\"。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 0a - 表示这是 \"支持的组\" 扩展"
},
{
"Tag": "li",
"content": "00 0c - 扩展信息长度为 0x0c(12) 字节 "
},
{
"Tag": "li",
"content": "00 0a - 曲线列表条目长度为 0x0a(10) 字节"
},
{
"Tag": "li",
"content": "00 17 - 代表 secp256r1 曲线"
},
{
"Tag": "li",
"content": "00 1d - 代表 x25519 曲线"
},
{
"Tag": "li",
"content": "00 18 - 代表 secp384r1 曲线"
},
{
"Tag": "li",
"content": "00 19 - 代表 secp521r1 曲线"
},
{
"Tag": "li",
"content": "10 00 - 代表 ffdhe2048 曲线"
}
]
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "16"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 类型为 0x16(22), 握手记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverHandshakeFinishedDatagram.json
================================================
[
"为了验证握手成功且没有被篡改过,服务器会创建一些验证数据给客户端确认。验证数据是基于所有握手信息的哈希值计算得到。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"头部信息字节",
{
"props": {
"className": "bytes"
},
"content": "2e"
},
[
"加密的 DTLS 数据包都以一个 \"统一的头部(unified header)\"开始。头部的第一个字节给出了头部和数据包的结构信息,以及解密时需要的信息。",
"值 0x2e 具有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "001", "固定位"],
["", "0", "头部中不存在连接 ID 字段(1则存在)"],
["", "1", "序列号在头部中占 2 字节长"],
["", "1", "头部中存在\"记录长度\"字段(0则不存在)"],
[
"低位",
"10",
"加密序列指示(Encryption epoch 2),现在密钥是握手时密钥"
]
]
}
}
]
],
[
"记录序号",
{
"props": {
"className": "bytes protected",
"title": "被加密"
},
"content": "0b b8"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00 03"
},
[
"记录序号是被加密了的,用以防止中间件误解(interpreting)或干扰数据包的排序。",
"加密是通过用 \"服务器端序号保护密钥\" 对每个数据包的有效载荷样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行 XOR 得到。",
"如果说的不够详细,这里有一个如何加密的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"server record number key\" from handshake keys calc step above\n$ key=7173fac51194e775001d625ef69d7c9f\n### sample is taken from 16 bytes of payload starting 5 bytes into the record\n$ sample=a44135732a099823b8a5f61a2b35ce92\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p\n\n0bbb\n\n### the above bytes are xor'd one-for-one into the bytes of the record number"
}
}
]
],
[
"记录长度",
{
"props": {
"className": "bytes"
},
"content": "00 3d"
},
[
"每个记录除非给出这个长度字段,否则对等端将认为数据报剩余的所有字节都是同一个记录的真实载荷。有了这一字段,则在一个数据报中可以发送好几个 DTLS 记录(尽管例子中的连接没有利用这个优势)。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 3d - 表示 DTLS 记录长度为 0x3d(61) 字节"
}
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手密钥\"加密"
},
"content": "a4 41 35 73 2a 09 98 23 b8 a5 f6 1a 2b 35 ce 92 1a 89 ab b1 52 f8 76 cd 26 79 7d c3 ed 73 d9 17 b2 99 c1 69 28 b9 cf 9e 58 d1 cd 58 68"
},
["这些数据使用服务器端的\"握手密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "6b 8b 90 ce 9f e6 45 4e 0c ef 9e fc 40 f2 39 7a"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,确认加密数据和记录头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"服务器端握手密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过密钥和已经用密钥加密的记录长度进行异或操作生成。在例子中 IV 为 3。",
"数据包开头的 5 字节(记录头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Server Handshake Keys Calc\" step\n$ key=004e03e64ab6cba6b542775ec230e20a\n$ iv=6d9924be044ee97c624913f2\n### from this record\n$ recdata=2e0003003d\n$ authtag=6b8b90ce9fe6454e0cef9efc40f2397a\n$ recordnum=3\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 14 00 00 20 00 04 00 00 00 00 00 20 1d 89 aa 62\n00000010 e5 f8 8a 0f c9 52 88 47 15 d8 ac b3 79 86 59 af\n00000020 b9 e7 78 9a 8d b2 b3 81 6b a4 52 46 16\n... snip ..."
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"握手消息头",
{
"props": {
"className": "bytes encrypted"
},
"content": "14 00 00 20"
},
[
"每个握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "14 - 握手消息类型 0x14 (finished)"
},
{
"Tag": "li",
"content": "00 00 20 - 紧接着的握手消息数据的长度 0x20 (32) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes encrypted"
},
"content": "00 04 00 00 00 00 00 20"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的 DTLS 记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 04 - DTLS 序列号 0x4(4)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 00 20 - 表明之后的记录碎片的长度为 0x20(32) 字节"
}
]
},
"在本文例子中,整个握手记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"验证数据",
{
"props": {
"className": "bytes encrypted"
},
"content": "1d 89 aa 62 e5 f8 8a 0f c9 52 88 47 15 d8 ac b3 79 86 59 af b9 e7 78 9a 8d b2 b3 81 6b a4 52 46"
},
[
"使用 \"服务器端生成握手密钥\" 步骤中的服务器握手时密钥和在这之前的每个握手记录(ClientHello 到 证书验证数据)的 SHA256 哈希值生成。",
{
"Tag": "pre",
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "finished_key = HKDF-Expand-Label(key: server_secret, label: \"finished\", ctx: \"\", len: 32)\nfinished_hash = SHA256(Client Hello ... Server Cert Verify)\nverify_data = HMAC-SHA256(key: finished_key, msg: finished_hash)"
}
]
},
{
"children": [
"在命令行中使用",
{
"Tag": "a",
"props": {
"href": "https://dtls.xargs.org/files/hkdf-dtls.sh"
},
"content": "原作者制作的 HKDF 命令行脚本"
},
",你也可以自己试试:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### find the hash of the conversation to this point, excluding\n### cleartext record headers, DTLS-only record headers,\n### or 1-byte decrypted record trailers\n$ fin_hash=$((\n cat record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';\n cat record-encext | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cert | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';\n ) | openssl sha256)\n$ sht_secret=8ad7990b9d249bcbaa0805d8d3f3ad2259e75f3a42c5d84db3ea3c6ee57b3d38\n$ fin_key=$(./hkdf-dtls expandlabel $sht_secret \"finished\" \"\" 32)\n$ echo $fin_hash | xxd -r -p | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key\n\n1d89aa62e5f88a0fc952884715d8acb3798659afb9e7789a8db2b3816ba45246"
}
}
]
],
[
"记录类型",
{
"props": {
"className": "bytes encrypted"
},
"content": "16"
},
[
"每一个加密的 DTLS 1.3 记录的最后一个字节都需要表明其真正的记录类型",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 类型为 0x16(22), 握手记录"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverHandshakeKeysCalc.json
================================================
[
"服务器现在拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,服务器使用了以下信息:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "客户端公钥(提取自 ClientHello 数据报)"
},
{
"Tag": "li",
"content": "服务器端私钥(之前准备密钥交换时生成)"
},
{
"Tag": "li",
"content": "ClientHello 和 ServerHello 的 SHA256 哈希值"
}
]
},
"首先,服务器需要找到共享的密文(shared secret),即密钥交换步骤的最终值。服务器通过使用 curve25519 算法将客户端的公钥乘以服务器的私钥,即可发现 32 字节的最终值是:",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624"
}
]
},
{
"children": [
"你可以使用",
{
"Tag": "a",
"props": { "href": "https://quic.xargs.org/files/curve25519-mult.c" },
"content": "原作者的脚本"
},
"快速验证结果:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ cc -o curve25519-mult curve25519-mult.c\n$ ./curve25519-mult server-ephemeral-private.key client-ephemeral-public.key | hexdump\n\n0000000 df 4a 29 1b aa 1e b7 cf a6 93 4b 29 b4 74 ba ad\n0000010 26 97 e2 9f 1f 92 0d cc 77 c8 a0 a0 88 44 76 24"
}
},
"然后,服务器计算到此为止的所有握手信息(ClientHello 和 ServerHello)的 SHA256 哈希值(哈希值不包括记录中的 DTLS 专用字节,即 0-12 和 17-24 的字节。忽略这些字节允许实现者在 TLS 和 DTLS 的实现之间共享代码)。这个 \"hello_hash\" 是",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "aee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7"
}
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ (cat captures/caps/record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';\n cat captures/caps/record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/') | openssl sha256\n\naee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7"
}
},
"然后,我们将哈希值和共享的签名信息进行一些密钥派生操作(key derivation operations),以防止可能的攻击:",
{
"Tag": "pre",
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "early_secret = HKDF-Extract(salt=00, key=00...)\nempty_hash = SHA256(\"\")\nderived_secret = HKDF-Expand-Label(key: early_secret, label: \"derived\", ctx: empty_hash, len: 32)\nhandshake_secret = HKDF-Extract(salt: derived_secret, key: shared_secret)\nclient_secret = HKDF-Expand-Label(key: handshake_secret, label: \"c hs traffic\", ctx: hello_hash, len: 32)\nserver_secret = HKDF-Expand-Label(key: handshake_secret, label: \"s hs traffic\", ctx: hello_hash, len: 32)\nclient_key = HKDF-Expand-Label(key: client_secret, label: \"key\", ctx: \"\", len: 16)\nserver_key = HKDF-Expand-Label(key: server_secret, label: \"key\", ctx: \"\", len: 16)\nclient_iv = HKDF-Expand-Label(key: client_secret, label: \"iv\", ctx: \"\", len: 12)\nserver_iv = HKDF-Expand-Label(key: server_secret, label: \"iv\", ctx: \"\", len: 12)\nclient_sn_key = HKDF-Expand-Label(key: client_secret, label: \"sn\", ctx: \"\", len: 16)\nserver_sn_key = HKDF-Expand-Label(key: server_secret, label: \"sn\", ctx: \"\", len: 16)"
}
]
},
{
"children": [
"在命令行中使用",
{
"Tag": "a",
"props": { "href": "https://dtls.xargs.org/files/hkdf-dtls.sh" },
"content": "原作者制作的 HKDF 命令行脚本"
},
",你也可以自己试试:"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "$ hello_hash=aee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7\n$ shared_secret=df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624\n$ zero_key=0000000000000000000000000000000000000000000000000000000000000000\n$ early_secret=$(./hkdf-dtls extract 00 $zero_key)\n$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')\n$ derived_secret=$(./hkdf-dtls expandlabel $early_secret \"derived\" $empty_hash 32)\n$ handshake_secret=$(./hkdf-dtls extract $derived_secret $shared_secret)\n$ csecret=$(./hkdf-dtls expandlabel $handshake_secret \"c hs traffic\" $hello_hash 32)\n$ ssecret=$(./hkdf-dtls expandlabel $handshake_secret \"s hs traffic\" $hello_hash 32)\n$ client_handshake_key=$(./hkdf-dtls expandlabel $csecret \"key\" \"\" 16)\n$ server_handshake_key=$(./hkdf-dtls expandlabel $ssecret \"key\" \"\" 16)\n$ client_handshake_iv=$(./hkdf-dtls expandlabel $csecret \"iv\" \"\" 12)\n$ server_handshake_iv=$(./hkdf-dtls expandlabel $ssecret \"iv\" \"\" 12)\n$ client_sn_key=$(./hkdf-dtls expandlabel $csecret \"sn\" \"\" 16)\n$ server_sn_key=$(./hkdf-dtls expandlabel $ssecret \"sn\" \"\" 16)\n$ echo client_key: $client_handshake_key\n$ echo client_iv: $client_handshake_iv\n$ echo server_key: $server_handshake_key\n$ echo server_iv: $server_handshake_iv\n$ echo client_sn_key: $client_sn_key\n$ echo server_sn_key: $server_sn_key\n\nclient_key: 6caa2633d5e48f10051e69dc45549c97\nclient_iv: 106dc6e393b7a9ea8ef29dd7\nserver_key: 004e03e64ab6cba6b542775ec230e20a\nserver_iv: 6d9924be044ee97c624913f2\nclient_sn_key: beed6218676635c2cb46a45694144fec\nserver_sn_key: 7173fac51194e775001d625ef69d7c9f"
}
},
"由此我们可以得到以下密钥以及向量:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"children": [
"客户端握手时密钥: ",
{
"Tag": "code",
"content": "6caa2633d5e48f10051e69dc45549c97"
}
]
},
{
"Tag": "li",
"children": [
"客户端握手时向量: ",
{
"Tag": "code",
"content": "106dc6e393b7a9ea8ef29dd7"
}
]
},
{
"Tag": "li",
"children": [
"客户端序号保护密钥(record number key): ",
{
"Tag": "code",
"content": "beed6218676635c2cb46a45694144fec"
}
]
},
{
"Tag": "li",
"children": [
"服务器端握手时密钥: ",
{
"Tag": "code",
"content": "004e03e64ab6cba6b542775ec230e20a"
}
]
},
{
"Tag": "li",
"children": [
"服务器端握手时向量: ",
{
"Tag": "code",
"content": "6d9924be044ee97c624913f2"
}
]
},
{
"Tag": "li",
"children": [
"服务器端序号保护密钥: ",
{
"Tag": "code",
"content": "7173fac51194e775001d625ef69d7c9f"
}
]
}
]
}
]
================================================
FILE: src/DTLS/serverHelloDatagram.json
================================================
[
"服务器回复 \"ServerHello\"。服务器提供的信息包括以下内容:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "服务器端随机数(在随后的握手时使用)"
},
{
"Tag": "li",
"content": "服务器端选择的加解密算法"
},
{
"Tag": "li",
"content": "用于密钥交换的公钥"
},
{
"Tag": "li",
"content": "服务器协商的具体协议的版本"
}
]
},
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"DTLS 记录头",
{
"props": {
"className": "bytes"
},
"content": "16 fe fd 00 00 00 00 00 00 00 00 00 62"
},
[
"每个 DTLS 记录都以一个 type、一些序列信息(seq info)和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "16 - 表示 TLS 记录类型 0x16(22, Handshake)"
},
{
"Tag": "li",
"content": "fe fd - 协议版本 (DTLS 1.2, 细节见下文)"
},
{
"Tag": "li",
"content": "00 00 - 密钥序列指示(key epoch,每次密钥更新时都会递增)"
},
{
"Tag": "li",
"content": "00 00 00 00 00 00 - DTLS 序列号 0x0(0)"
},
{
"Tag": "li",
"content": "00 62 - 紧接着的数组载荷长度 0x62(98) 字节"
}
]
},
"DTLS 版本的编码方式是将协议版本分成几个部分,然后取每个部分的补码。(因此 \"1.3\" 变成 {1, 3},变成字节 0xFE 0xFC)。这种补码技术使 DTLS 版本与 TLS 版本有所差别。",
"由于已经创建和部署的网络中间件(middleboxes)不允许它们所不承认的协议版本通过,因此所有 DTLS 1.3 会话在未加密的记录中都会显示为 DTLS 1.2(0xFE 0xFD)。"
]
],
[
"TLS 握手记录头",
{
"props": {
"className": "bytes"
},
"content": "02 00 00 56"
},
[
"每个 TLS 握手消息都以一个 type 和一个 len 开始。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "02 - 握手消息类型 0x02 (ServerHello)"
},
{
"Tag": "li",
"content": "00 00 56 - 紧接着的握手消息数据的长度 0x56 (86) 字节"
}
]
}
]
],
[
"用于重建握手顺序的信息(Handshake Reconstruction Data)",
{
"props": {
"className": "bytes"
},
"content": "00 00 00 00 00 00 00 56"
},
[
"因为 UDP (或其他数据报协议)不保证交付或排序,而且数据报的长度可能比需要发送的握手记录长度要小。因此 DTLS 必须提供一定的信息,以支持在数据丢失、包重排序或有记录碎片的情况下,使得对等端(peer)能够重新构建一条正确的数据记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 00 - DTLS 序列号 0x0(0)"
},
{
"Tag": "li",
"content": "00 00 00 - 表明记录碎片的偏移量为 0x00(0) 字节"
},
{
"Tag": "li",
"content": "00 00 56 - 表明之后的记录碎片的长度为 0x56(86) 字节"
}
]
},
"在本文例子中,整个握手记录记录的长度要短于一个 UDP 数据报的可承载长度,因此偏移量为零,且长度为整个握手记录长。"
]
],
[
"服务器端 DTLS 版本号(废弃)",
{
"props": {
"className": "bytes"
},
"content": "fe fd"
},
[
"DTLS 版本的编码方式是将协议版本分成几个部分,然后取每个部分的补码。(因此 \"1.3\" 变成 {1, 3},变成字节 0xFE 0xFC)。这种补码技术使 DTLS 版本与 TLS 版本有所差别。",
"由于已经创建和部署的网络中间件(middleboxes)不允许它们所不承认的协议版本通过,因此所有 DTLS 1.3 会话在未加密的记录中都会显示为 DTLS 1.2(0xFE 0xFD)。所有的 DTLS 1.3 及以上版本的会话需要通过后面提到的\"支持的版本\"拓展协商真实版本号。"
]
],
[
"服务器端随机数",
{
"props": {
"className": "bytes"
},
"content": "70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f"
},
[
"服务器端提供的 32 字节的随机数。这个数将在之后的会话中使用。在本文的例子中,我们暂时将一个方便记忆的字符串当作随机数。"
]
],
[
"会话 ID (废弃)",
{
"props": {
"className": "bytes"
},
"content": "00"
},
[
"这是一个废弃(legacy)字段,不在 DTLS 1.3 中使用。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 - 0 字节的会话 ID"
}
]
}
]
],
[
"所选择的加解密算法",
{
"props": {
"className": "bytes"
},
"content": "13 01"
},
[
"服务器从客户端给出的选项列表中选择的加解密算法 0x1301(TLS_AES_128_GCM_SHA256)。"
]
],
[
"压缩算法",
{
"props": {
"className": "bytes"
},
"content": "00"
},
["服务器从客户端给出的选项列表中选择的压缩算法 0x00(null)。"]
],
[
"扩展的长度",
{
"props": {
"className": "bytes"
},
"content": "00 2e"
},
[
"服务器向客户端返回的扩展有序列表的长度。因为服务器被禁止回复 ClientHello 消息中不存在的扩展,因此服务器知道客户端将理解并支持列出的所有扩展。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 2e - 扩展列表的长度为 0x2E(46) 字节"
}
]
},
"每个扩展将以两个字节开始,表明它是哪个扩展。然后是两个字节的内容长度字段,最后是扩展的具体内容。"
]
],
[
"扩展 - 算法公钥列表",
{
"props": {
"className": "bytes"
},
"content": "00 33 00 24 00 1d 00 20 9f d7 ad 6d cf f4 29 8d d3 f9 6d 5b 1b 2a f9 10 a0 53 5b 14 88 d7 f8 fa bb 34 9a 98 28 80 b6 15"
},
[
"服务器使用和客户端发送公钥时相同的算法发送一个自己的公钥。一旦这个被发送,加密密钥就可以被计算出来。其余的握手则将被加密。而不用像以前的协议版本,以透明的方式发送握手记录。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 33 - 表示这是 \"算法公钥列表\" 扩展"
},
{
"Tag": "li",
"content": "00 24 - 算法公钥列表长度为 0x24(36) 字节"
},
{
"Tag": "li",
"content": "00 1d - 代表 x25519 算法(例子中为通过 curve25519 算法进行密钥交换)"
},
{
"Tag": "li",
"content": "00 20 - 公钥长度为 0x20(32) 字节"
},
{
"Tag": "li",
"content": "9f d7 ... b6 15 - \"服务器端准备密钥交换\" 步骤中生成的公钥"
}
]
}
]
],
[
"扩展 - 支持的版本",
{
"props": {
"className": "bytes"
},
"content": "00 2b 00 02 fe fc"
},
[
"客户端表明其支持 DTLS 1.3。由于兼容性的原因,这被放在一个扩展中,而不是上面的客户端版本字段。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 2b - 表示这是 \"支持的版本\" 扩展"
},
{
"Tag": "li",
"content": "00 02 - 扩展信息长度为 0x02(2) 字节 "
},
{
"Tag": "li",
"content": "fe fc - 代表 DTLS 1.3"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/DTLS/serverKeyExchangeGeneration.json
================================================
[
"服务器端也需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。",
{
"Tag": "p",
"children": [
"学习 DTLS 并不需要深入了解,但你可以从",
{
"Tag": "a",
"props": { "href": "https://cangsdarm.github.io/illustrate/x25519" },
"content": "X25519 密钥交换算法"
},
"获取涉及到的密钥交换算法的具体解释。"
]
},
[
"**私钥**是 0 到 ",
{
"Tag": "Math",
"content": "2^256-1"
},
" 之间的一个随机整数(32bytes, 256bits)",
"。为方便后续解释,假设我们生成的私钥是:"
],
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
}
]
},
"**公钥**由上面提到的 X25519 密钥交换算法生成。例子中计算出的公钥应如下所示:",
{
"Tag": "pre",
"props": {
"className": "ind2"
},
"children": [
{
"Tag": "code",
"props": { "className": "longboi" },
"content": "9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615"
}
]
},
"公钥的计算也可以在命令行中运行以下命令得到:",
{
"Tag": "CodeSample",
"props": {
"code": "### requires openssl 1.1.0 or higher\n$ openssl pkey -noout -text < server-ephemeral-private.key\n\nX25519 Private-Key:\npriv:\n 90:91:92:93:94:95:96:97:98:99:9a:9b:9c:9d:9e:\n 9f:a0:a1:a2:a3:a4:a5:a6:a7:a8:a9:aa:ab:ac:ad:\n ae:af\npub:\n 9f:d7:ad:6d:cf:f4:29:8d:d3:f9:6d:5b:1b:2a:f9:\n 10:a0:53:5b:14:88:d7:f8:fa:bb:34:9a:98:28:80:\n b6:15"
}
}
]
================================================
FILE: src/Datagram/index.jsx
================================================
import React from "react";
import classes from "./style.module.css";
const Datagram = ({ label = "", children }) => {
return (
<div className={classes["datagram"]}>
<span className={classes["label"]}>{label}</span>
{children}
</div>
);
};
export default Datagram;
================================================
FILE: src/Datagram/style.module.css
================================================
/***** datagram borders *****/
.datagram {
max-width: 800px;
border-radius: 1.2em;
border: 1px solid black;
padding: 5px 15px;
margin: 0.8em auto;
}
.datagram > .label {
font-size: 0.8em;
font-weight: bold;
}
================================================
FILE: src/Footer/index.jsx
================================================
import React from "react";
const Footer = ({ desc, mother, references }) => {
return (
<>
<div>
<p
style={{ textAlign: "center" }}
dangerouslySetInnerHTML={{ __html: desc }}
/>
</div>
<div>
<p style={{ textAlign: "center" }}>
原作者{" "}
<a href="https://twitter.com/xargsnotbombs" target="_blank">
(Twitter)@XargsNotBombs
</a>
,{" "}
<a href={mother} target="_blank">
{mother}
</a>
</p>
</div>
<div>
<p style={{ textAlign: "center" }}>
译者{" "}
<a href="https://github.com/cangSDARM" target="_blank">
(Github)@AllenLee
</a>
, 源代码托管在{" "}
<a href="https://github.com/cangSDARM/illustrate/" target="_blank">
https://github.com/cangSDARM/illustrate/
</a>
</p>
</div>
{references && (
<div>
<dl
style={{
display: "flex",
flexDirection: "column",
maxWidth: "80vw",
margin: "0 auto",
}}
>
<span style={{ alignSelf: "start" }}>参考:</span>
{references.map((ref) => (
<li key={ref.title}>
{ref.title}:<a href={ref.href}>{ref.href}</a>
{ref.quote}
</li>
))}
</dl>
</div>
)}
</>
);
};
export default Footer;
================================================
FILE: src/Header/index.jsx
================================================
import React from "react";
import clsx from "clsx";
import classes from "./style.module.css";
import { jump } from "../utils";
const getRouterUsingPath = (routers = [], path = "") =>
routers.find((rt) => rt.href.startsWith(path)) || routers[0];
const Header = ({ routers = [], onRouterChange, base = "" }) => {
const [pathName, setPathName] = React.useState(globalThis.location.pathname);
const handleRouterChange = React.useCallback((rt) => {
onRouterChange?.(rt);
setPathName(rt.href);
}, []);
React.useEffect(() => {
let curPath = globalThis.location.pathname;
if (routers.findIndex((rt) => curPath.startsWith(rt.href)) < 0) {
// try state for github-pages
curPath = globalThis.history.state?.path || "";
}
if (routers.findIndex((rt) => curPath.startsWith(rt.href)) < 0) {
curPath = routers[0];
jump(curPath.href);
}
handleRouterChange(getRouterUsingPath(routers, curPath));
}, []);
React.useEffect(() => {
const listener = (e) => {
const curPath = globalThis.document.location.pathname;
setPathName(curPath);
handleRouterChange(getRouterUsingPath(routers, curPath));
};
window?.addEventListener("popstate", listener);
return () => window?.removeEventListener("popstate", listener);
}, []);
return (
<div className={classes.header}>
{routers.map((rt) => (
<a
key={rt.href}
href={rt.href}
className={clsx(rt.href === pathName && classes["this-page"])}
onClick={(e) => {
if (rt.href === pathName) return;
e.stopPropagation();
e.preventDefault();
jump(rt.href);
handleRouterChange(rt);
}}
>
{rt.label}
</a>
))}
</div>
);
};
export default Header;
================================================
FILE: src/Header/style.module.css
================================================
.header {
width: 100%;
margin: 0;
padding: 2px;
border-bottom: 1px solid grey;
background-color: bisque;
color: #444;
line-height: 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
text-align: right;
}
.header a,
.header a:visited,
.header a:hover {
text-decoration: none;
color: #444;
}
.header a.this-page:before {
content: "❧\00a0";
}
.header a,
.header span {
margin-right: 0.5em;
}
.header a.this-page {
font-weight: bold;
}
================================================
FILE: src/Intro/index.jsx
================================================
import classes from "./style.module.css";
import Balancer from "react-wrap-balancer";
const Intro = ({ title = "", subtitle = "", desc = "", intro = "" }) => {
return (
<section>
<h1>
<Balancer>{title}</Balancer>
</h1>
{subtitle && (
<h3>
<Balancer>{subtitle}</Balancer>
</h3>
)}
{desc && (
<h5>
<Balancer>{desc}</Balancer>
</h5>
)}
{intro && (
<div className={classes["intro-block"]}>
<p>
<Balancer>{intro}</Balancer>
</p>
</div>
)}
</section>
);
};
export default Intro;
================================================
FILE: src/Intro/style.module.css
================================================
h1:after {
content: "\00a0❧";
}
h1:before {
content: "❧\00a0";
}
h1,
h3,
h5 {
text-align: center;
padding: 10px;
max-width: 800px;
margin: 0 auto;
}
h5 {
font-style: italic;
font-family: serif;
}
.intro-block {
max-width: 600px;
margin: 0.5em auto 1em;
}
.intro-block p {
text-align: center;
}
================================================
FILE: src/QUIC/clientAck.json
================================================
[
"客户端再发送一个 \"初始\" 的数据包。其中包含服务器的最后一个 \"初始\" 数据包的 ACK。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据包头字节",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "cf"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "c0"
},
[
"数据包以一个头字节开始,该字节应用了头保护。头部保护用于隐藏数据包序号和其他信息,使其不被外界观察到。",
"包头保护是通过用\"数据包头保护密钥\"对每个数据包的有效载荷的样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行异或(XOR)操作得到的。对于像这样的\"长\"格式数据包,受保护的部分是这个字节的低 4 位,以及数据包序号的字节(见下文)。",
"这里有一个关于如何计算出加密头字节的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client header protection key\" from handshake keys calc step above\n$ key=6df4e9d737cdf714711d7c617ee82981\n### sample is taken from 16 bytes of payload starting\n### 4 bytes past the first byte of the packet number\n$ sample=ed1f7b0555cdb783fbdf5b52724b7d29\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p\n\n8f57c29e79\n\n### first byte of result is xor'd into lower 4 bits of this byte,\n### remaining bytes are xor'd one-for-one into the bytes of\n### the packet number (which in this packet is only one byte)"
}
},
"解密出的字节 0xC0 中的位有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "1", "Long Header 格式"],
["", "1", "固定位(总是被置1)"],
["", "00", "数据包类型:初始化"],
["", "00", "保留(总是被置0)"],
[
"低位",
"00",
"数据包序号长度(表示下面的 \"数据包序号\" 将有一个字节的长度,默认值)"
]
]
}
}
]
],
[
"QUIC 版本号",
{
"props": {
"className": "bytes"
},
"content": "00 00 00 01"
},
["QUIC的版本是:0x1"]
],
[
"目的地连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 73 5f 63 69 64"
},
[
"服务器的标识 ID (服务器端的源连接标识 ID)",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的目的地连接标识 ID 的长度"
},
{
"Tag": "li",
"content": "73 5f 63 69 64 - 实际的目的地连接标识 ID(\"s_cid\")"
}
]
}
]
],
[
"源连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 63 5f 63 69 64"
},
[
"客户端使用这个字段来表明它选择的传输给服务器的源连接标识 ID。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的源连接标识 ID 的长度(5bytes)"
},
{
"Tag": "li",
"content": "63 5f 63 69 64 - 实际的源连接标识 ID (\"c_cid\")"
}
]
}
]
],
[
"令牌",
{
"props": {
"className": "bytes"
},
"content": "00"
},
[
"客户端在某些情况下可以使用这个字段来提供服务器所要求的令牌,例如证明其连接尝试不是欺骗。此时,没有令牌需要提供,该字段为空。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "00 - 紧跟着的令牌长度(0bytes)"
}
]
}
]
],
[
"数据包长度",
{
"props": {
"className": "bytes"
},
"content": "40 17"
},
[
"客户端指明后续数据包数据的长度。这个字段是一个长度可变的整数——第一个字节的前两位表示该整数中总共有多少个字节。",
"此时,第一个字节以 \"0 1\"(0x4)这两个位开始,表示该整数共两个字节。其余的位给出数字 0x17,表示 23 个字节的有效载荷。"
]
],
[
"数据包序号",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "56"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "01"
},
[
"该字节应用了头保护。详见 \"数据包头字节\"。",
"这个字节的未受保护的值为 0x01,表明它是第 1 号包,或者说是客户端发送的第二个\"初始\"包。",
"这个数据也有可能被截断。发送端点通过几个步骤截断:①计算已发送的最高的序号和未确认的最低的序号之间的差值;②出于安全考虑将差值加倍并四舍五入;③计算它在明确表示两端之间的序号的前提下可以从序号的高位删除的字节数;④截断编码后的数据包序号直至长度满足该字节数。而接收端点根据会它最近看到的数据包号码填入完整的序号。",
{
"children": [
"由于我们的例子对话发送的数据包很小(少于 64 个字节),所以这种截断不会在本文中出现。详情见 ",
{
"Tag": "a",
"props": {
"href": "https://www.rfc-editor.org/rfc/rfc9000.html#section-17.1"
},
"content": "RFC 9000"
},
"。"
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"初始密钥\"加密"
},
"content": "6e 1f 98 ed 1f 7b"
},
["这些数据使用客户端的\"初始密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "05 55 cd b7 83 fb df 5b 52 72 4b 7d 29 f0 af e3"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,用以确认加密数据和数据包头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端初始密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过对密钥和数据包序号进行异或操作生成。在例子中 IV 为 1。",
"数据包开头的 21 字节(数据包头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Initial Keys Calc\" step\n$ key=b14b918124fda5c8d79847602fa3520b\n$ iv=ddbc15dea80925a55686a7df\n### from this record\n$ recdata=c00000000105735f63696405635f63696400401701\n$ authtag=0555cdb783fbdf5b52724b7d29f0afe3\n$ recordnum=1\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 02 00 40 81 00 00 |..@...|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"ACK 帧",
{
"props": {
"className": "bytes encrypted"
},
"content": "02 00 40 81 00 00"
},
[
"服务器确认收到客户端的初始数据包0。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "02 - 表明帧类型 ACK"
},
{
"Tag": "li",
"content": "00 - largest_acknowledged: 被确认的最大数据包"
},
{
"Tag": "li",
"children": [
"40 81 - ack_delay: 变长的整数。给出这个 ack 被延迟发送的时间,单位是微秒。",
"通过计算式子:2",
{
"Tag": "sup",
"content": "ack_delay_exponent"
},
" 得到。其中 ack_delay_exponent = 129 * 8 = 1,032 (µseconds)。"
]
},
{
"Tag": "li",
"content": "00 - ack_range_count: 额外的 ACK 帧数据长度 0x(0)"
},
{
"Tag": "li",
"content": "00 - first_ack_range: 可变长度的整数。给出在 largest_acknowledged 之前被确认过的数据包数量。"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/QUIC/clientApplicationKeysCalc.json
================================================
[
"客户端现在也可以计算应用会话时的密钥了。如果计算正确,结果应和服务器端的一致:",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"children": [
"客户端会话密钥: ",
{
"Tag": "code",
"content": "e010a295f0c2864f186b2a7e8fdc9ed7"
}
]
},
{
"Tag": "li",
"children": [
"客户端会话向量: ",
{
"Tag": "code",
"content": "eb3fbc384a3199dcf6b4c808"
}
]
},
{
"Tag": "li",
"children": [
"客户端会话数据包头保护密钥: ",
{
"Tag": "code",
"content": "8a6a38bc5cc40cb482a254dac68c9d2f"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话密钥: ",
{
"Tag": "code",
"content": "fd8c7da9de1b2da4d2ef9fd5188922d0"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话向量: ",
{
"Tag": "code",
"content": "02f6180e4f4aa456d7e8a602"
}
]
},
{
"Tag": "li",
"children": [
"服务器端会话数据包头保护密钥: ",
{
"Tag": "code",
"content": "b7f6f021453e52b58940e4bba72a35d4"
}
]
}
]
}
]
================================================
FILE: src/QUIC/clientApplicationPacket1.json
================================================
[
"客户端发送其第一个握手后的数据包,即第一个实际包含应用内容的会话过程数据包。其中包含内容为 \"ping\" 的流数据。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据包头字节",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "4e"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "40"
},
[
"数据包以一个头字节开始,该字节应用了头保护。头部保护用于隐藏数据包序号和其他信息,使其不被外界观察到。",
"包头保护是通过用\"数据包头保护密钥\"对每个数据包的有效载荷的样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行异或(XOR)操作得到的。对于像这样的\"长\"格式数据包,受保护的部分是这个字节的低 4 位,以及数据包序号的字节(见下文)。",
"这里有一个关于如何计算出加密头字节的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client header protection key\" from application keys calc step above\n$ key=8a6a38bc5cc40cb482a254dac68c9d2f\n### sample is taken from 16 bytes of payload starting\n### 4 bytes past the first byte of the packet number\n$ sample=e66e8ee950ba8b8ed10cba39a06ab7b0\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p\n\n4e1e62a65d\n\n### first byte of result is xor'd into lower 5 bits of this byte,\n### remaining bytes are xor'd one-for-one into the bytes of\n### the packet number (which in this packet is only one byte)"
}
},
"解密出的字节 0x40 中的位有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "0", "Short Header 格式"],
["", "1", "固定位(总是被置1)"],
[
"",
"0",
"可选的 \"Spin\" 位。用于允许观察者测量 RTT,但 QUIC 未使用。"
],
["", "00", "保留(总是被置0)"],
["", "0", "密钥相位位(key phase bit), 密钥发生轮替时置1"],
[
"低位",
"00",
"数据包序号长度(表示下面的 \"数据包序号\" 将有一个字节的长度,默认值)"
]
]
}
}
]
],
[
"目的地连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "73 5f 63 69 64"
},
[
"服务器端的标识 ID (服务器端的源连接标识 ID)",
"注意此时标识 ID 的长度(以及本应在后面的源连接标识 ID)已经省略。对等端(peer)在这之前应该且必须知道标识 ID 长度,该长度在会话过程中要么一直被省略,要么在标识 ID 中编码长度。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "73 5f 63 69 64 - 实际的目的地连接标识 ID(\"s_cid\")"
}
]
}
]
],
[
"数据包序号",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "1e"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00"
},
[
"该字节应用了头保护。详见 \"数据包头字节\"。",
"这个字节的未受保护的值为 0x00,表明它是第 0 号包,或者说是客户端发送的第一个\"会话\"包。",
"这个数据也有可能被截断。发送端点通过几个步骤截断:①计算已发送的最高的序号和未确认的最低的序号之间的差值;②出于安全考虑将差值加倍并四舍五入;③计算它在明确表示两端之间的序号的前提下可以从序号的高位删除的字节数;④截断编码后的数据包序号直至长度满足该字节数。而接收端点根据会它最近看到的数据包号码填入完整的序号。",
{
"children": [
"由于我们的例子对话发送的数据包很小(少于 64 个字节),所以这种截断不会在本文中出现。详情见 ",
{
"Tag": "a",
"props": {
"href": "https://www.rfc-editor.org/rfc/rfc9000.html#section-17.1"
},
"content": "RFC 9000"
},
"。"
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "cc 91 70 e6 6e 8e e9 50 ba"
},
["这些数据使用客户端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "8b 8e d1 0c ba 39 a0 6a b7 b0 67 0a 50 ef 68 e6"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,用以确认加密数据和数据包头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端生成会话密钥\" 步骤中产生的会话密钥和会话向量(IVs)加密。IVs 通过对密钥和数据包序号进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 16 字节(数据包头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Application Keys Calc\" step\n$ key=e010a295f0c2864f186b2a7e8fdc9ed7\n$ iv=eb3fbc384a3199dcf6b4c808\n### from this record\n$ recdata=40735f63696400\n$ authtag=8b8ed10cba39a06ab7b0670a50ef68e6\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 0f 00 00 40 04 70 69 6e 67 |...@.ping|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"流数据帧标识",
{
"props": {
"className": "bytes encrypted"
},
"content": "0f"
},
[
"客户端用此表明它发送的是数据流。流是 QUIC 连接中发送所有应用数据的机制,类似于单个 TCP 连接。",
"数据流的帧类型标识是一个范围在 0x8 到 0xf 的数字(二进制表示应该是:0b00001xxx),外加额外的可变位提供关于流的额外信息:",
{
"Tag": "Table",
"props": {
"headers": ["位掩码", "含义"],
"data": [
["0x4", "OFF: 该帧中存在一个\"偏移\"字段(否则偏移量为0)"],
[
"0x2",
"LEN:该帧中存在一个\"长度\"字段(否则对等端应消耗帧中的所有数据)。"
],
[
"0x1",
"FIN:该帧包含该数据流的最终数据,发送方已经完成了对它的写入。"
]
]
}
},
"在本文例子中,发送方表示所有三个含义:将有一个偏移字段、一个长度字段,以及此帧包含该条数据流的最终数据。"
]
],
[
"流的 ID 序号",
{
"props": {
"className": "bytes encrypted"
},
"content": "00"
},
[
"客户端给出流的 ID 序号。ID 序号按顺序增加,且最后两个比特表示流的类型和方向:",
{
"Tag": "Table",
"props": {
"headers": ["位掩码", "含义"],
"data": [
["0x2", "表示流是双向的(0)还是单向的(1)。"],
[
"0x1",
"表示流是由客户端(0)还是服务器端(1)打开的。"
]
]
}
},
"在本文例子中,表示流的 ID 序号为 0,且是客户端打开的双向数据流。"
]
],
[
"流的偏移",
{
"props": {
"className": "bytes encrypted"
},
"content": "00"
},
[
"一个可变长度的整数,表示流数据的偏移量。",
"在本文例子中,一个单字节的整数显示偏移量为 0。"
]
],
[
"流的长度",
{
"props": {
"className": "bytes encrypted"
},
"content": "40 04"
},
[
"一个可变长度的整数,表示流数据的长度。",
"在本文例子中,前两个比特(0 1)表示一个两字节的整数,其余比特表明流的长度为 4 字节。"
]
],
[
"数据",
{
"props": {
"className": "bytes encrypted"
},
"content": "70 69 6e 67"
},
[
"客户端发送的数据,字符串\"ping\""
]
]
]
}
}
]
================================================
FILE: src/QUIC/clientApplicationPacket2.json
================================================
[
"客户端接收后,需要发送一个用于告知服务器已收到数据的 \"ACK\" 数据包。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据包头字节",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "5a"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "40"
},
[
"数据包以一个头字节开始,该字节应用了头保护。头部保护用于隐藏数据包序号和其他信息,使其不被外界观察到。",
"包头保护是通过用\"数据包头保护密钥\"对每个数据包的有效载荷的样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行异或(XOR)操作得到的。对于像这样的\"长\"格式数据包,受保护的部分是这个字节的低 4 位,以及数据包序号的字节(见下文)。",
"这里有一个关于如何计算出加密头字节的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client header protection key\" from application keys calc step above\n$ key=8a6a38bc5cc40cb482a254dac68c9d2f\n### sample is taken from 16 bytes of payload starting\n### 4 bytes past the first byte of the packet number\n$ sample=90588b44b10d7cd32b03e34502802f25\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p\n\n1ac9ce3a7a0\n\n### first byte of result is xor'd into lower 5 bits of this byte,\n### remaining bytes are xor'd one-for-one into the bytes of\n### the packet number (which in this packet is only one byte)"
}
},
"解密出的字节 0x40 中的位有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "0", "Short Header 格式"],
["", "1", "固定位(总是被置1)"],
[
"",
"0",
"可选的 \"Spin\" 位。用于允许观察者测量 RTT,但 QUIC 未使用。"
],
["", "00", "保留(总是被置0)"],
["", "0", "密钥相位位(key phase bit), 密钥发生轮替时置1"],
[
"低位",
"00",
"数据包序号长度(表示下面的 \"数据包序号\" 将有一个字节的长度,默认值)"
]
]
}
}
]
],
[
"目的地连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "73 5f 63 69 64"
},
[
"服务器端的标识 ID (服务器端的源连接标识 ID)",
"注意此时标识 ID 的长度(以及本应在后面的源连接标识 ID)已经省略。对等端(peer)在这之前应该且必须知道标识 ID 长度,该长度在会话过程中要么一直被省略,要么在标识 ID 中编码长度。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "73 5f 63 69 64 - 实际的目的地连接标识 ID(\"s_cid\")"
}
]
}
]
],
[
"数据包序号",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "c8"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "01"
},
[
"该字节应用了头保护。详见 \"数据包头字节\"。",
"这个字节的未受保护的值为 0x01,表明它是第 1 号包,或者说是客户端发送的第二个\"会话\"包。",
"这个数据也有可能被截断。发送端点通过几个步骤截断:①计算已发送的最高的序号和未确认的最低的序号之间的差值;②出于安全考虑将差值加倍并四舍五入;③计算它在明确表示两端之间的序号的前提下可以从序号的高位删除的字节数;④截断编码后的数据包序号直至长度满足该字节数。而接收端点根据会它最近看到的数据包号码填入完整的序号。",
{
"children": [
"由于我们的例子对话发送的数据包很小(少于 64 个字节),所以这种截断不会在本文中出现。详情见 ",
{
"Tag": "a",
"props": {
"href": "https://www.rfc-editor.org/rfc/rfc9000.html#section-17.1"
},
"content": "RFC 9000"
},
"。"
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"会话密钥\"加密"
},
"content": "67 e0 b4 90 58"
},
["这些数据使用客户端的\"会话密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "8b 44 b1 0d 7c d3 2b 03 e3 45 02 80 2f 25 a1 93"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,用以确认加密数据和数据包头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端生成会话密钥\" 步骤中产生的会话密钥和会话向量(IVs)加密。IVs 通过对密钥和数据包序号进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 16 字节(数据包头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Application Keys Calc\" step\n$ key=e010a295f0c2864f186b2a7e8fdc9ed7\n$ iv=eb3fbc384a3199dcf6b4c808\n### from this record\n$ recdata=40735f63696401\n$ authtag=8b44b10d7cd32b03e34502802f25a193\n$ recordnum=1\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 02 00 0b 00 00 |.....|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"ACK 帧",
{
"props": {
"className": "bytes encrypted"
},
"content": "02 00 0b 00 00"
},
[
"客户端确认收到服务器端的会话数据包0。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "02 - 表明帧类型 ACK"
},
{
"Tag": "li",
"content": "00 - largest_acknowledged: 被确认的最大数据包"
},
{
"Tag": "li",
"children": [
"0b - ack_delay: 变长的整数。给出这个 ack 被延迟发送的时间,单位是微秒。",
"通过计算式子:2",
{
"Tag": "sup",
"content": "ack_delay_exponent"
},
" 得到。其中 ack_delay_exponent = 11 * 8 = 88 (µseconds)。"
]
},
{
"Tag": "li",
"content": "00 - ack_range_count: 额外的 ACK 帧数据长度 0x(0)"
},
{
"Tag": "li",
"content": "00 - first_ack_range: 可变长度的整数。给出在 largest_acknowledged 之前被确认过的数据包数量。"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/QUIC/clientHandshake1.json
================================================
[
"紧跟着,客户端需要发送一个用于 \"握手\" 的数据包。这个数据包包含对服务器端 \"握手\" 数据包的 ACK。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据包头字节",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "ee"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "e0"
},
[
"数据包以一个头字节开始,该字节应用了头保护。头部保护用于隐藏数据包序号和其他信息,使其不被外界观察到。",
"包头保护是通过用\"数据包头保护密钥\"对每个数据包的有效载荷的样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行异或(XOR)操作得到的。对于像这样的\"长\"格式数据包,受保护的部分是这个字节的低 4 位,以及数据包序号的字节(见下文)。",
"这里有一个关于如何计算出加密头字节的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client header protection key\" from handshake keys calc step above\n$ key=84b3c21cacaf9f54c885e9a506459079\n### sample is taken from 16 bytes of payload starting\n### 4 bytes past the first byte of the packet number\n$ sample=c6cc12512d7eda141ec057b804d30feb\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p\n\n5e8c3ee850\n\n### first byte of result is xor'd into lower 4 bits of this byte,\n### remaining bytes are xor'd one-for-one into the bytes of\n### the packet number (which in this packet is only one byte)"
}
},
"解密出的字节 0xE0 中的位有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "1", "Long Header 格式"],
["", "1", "固定位(总是被置1)"],
["", "10", "数据包类型:握手"],
["", "00", "保留(总是被置0)"],
[
"低位",
"00",
"数据包序号长度(表示下面的 \"数据包序号\" 将有一个字节的长度,默认值)"
]
]
}
}
]
],
[
"QUIC 版本号",
{
"props": {
"className": "bytes"
},
"content": "00 00 00 01"
},
["QUIC的版本是:0x1"]
],
[
"目的地连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 73 5f 63 69 64"
},
[
"服务器的标识 ID (服务器端的源连接标识 ID)",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的目的地连接标识 ID 的长度"
},
{
"Tag": "li",
"content": "73 5f 63 69 64 - 实际的目的地连接标识 ID(\"s_cid\")"
}
]
}
]
],
[
"源连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 63 5f 63 69 64"
},
[
"客户端使用这个字段来表明它选择的传输给服务器的源连接标识 ID。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的源连接标识 ID 的长度(5bytes)"
},
{
"Tag": "li",
"content": "63 5f 63 69 64 - 实际的源连接标识 ID (\"c_cid\")"
}
]
}
]
],
[
"数据包长度",
{
"props": {
"className": "bytes"
},
"content": "40 16"
},
[
"客户端指明后续数据包数据的长度。这个字段是一个长度可变的整数——第一个字节的前两位表示该整数中总共有多少个字节。",
"此时,第一个字节以 \"0 1\"(0x4)这两个位开始,表示该整数共两个字节。其余的位给出数字 0x16,表示 22 个字节的有效载荷。"
]
],
[
"数据包序号",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密"
},
"content": "8c"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "00"
},
[
"该字节应用了头保护。详见 \"数据包头字节\"。",
"这个字节的未受保护的值为 0x00,表明它是第 0 号包,或者说是客户端发送的第一个\"握手\"包。",
"这个数据也有可能被截断。发送端点通过几个步骤截断:①计算已发送的最高的序号和未确认的最低的序号之间的差值;②出于安全考虑将差值加倍并四舍五入;③计算它在明确表示两端之间的序号的前提下可以从序号的高位删除的字节数;④截断编码后的数据包序号直至长度满足该字节数。而接收端点根据会它最近看到的数据包号码填入完整的序号。",
{
"children": [
"由于我们的例子对话发送的数据包很小(少于 64 个字节),所以这种截断不会在本文中出现。详情见 ",
{
"Tag": "a",
"props": {
"href": "https://www.rfc-editor.org/rfc/rfc9000.html#section-17.1"
},
"content": "RFC 9000"
},
"。"
]
}
]
],
[
"加密的数据载荷",
{
"props": {
"className": "bytes encrypted",
"title": "被\"握手时密钥\"加密"
},
"content": "b1 95 1f c6 cc"
},
["这些数据使用客户端的\"握手时密钥\"进行加密。"]
],
[
"AEAD 鉴别标签",
{
"props": {
"className": "bytes"
},
"content": "12 51 2d 7e da 14 1e c0 57 b8 04 d3 0f eb 51 5b"
},
[
{
"children": [
"这是 ",
{
"Tag": "a",
"props": {
"href": "https://zhuanlan.zhihu.com/p/28566058"
},
"content": "AEAD 算法"
},
"的鉴别标签,用以确认加密数据和数据包头的完整性。它由加密算法产生,并由解密算法消耗。"
]
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"data": [
[
"",
{
"props": {
"className": "decryption-header"
},
"content": "解密后的数据载荷"
},
[
{ "Tag": "h4", "content": "解密" },
"数据被 \"客户端握手时密钥计算\" 步骤中产生的初始密钥和初始向量(IVs)加密。IVs 通过对密钥和数据包序号进行异或操作生成。在例子中 IV 为 0。",
"数据包开头的 20 字节(数据包头)还会作为解密过程解密成功时必须满足的认证条件。",
{
"children": [
"openssl 命令行工具还不支持 AEAD 算法加解密(AEAD ciphers),你可以使用作者的命令行工具来",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_decrypt.c"
},
"content": "解密"
},
"和",
{
"Tag": "a",
"props": {
"href": "https://quic.xargs.org/files/aes_128_gcm_encrypt.c"
},
"content": "加密"
},
"这些数据。"
]
},
{
"Tag": "CodeSample",
"props": {
"code": "### from the \"Handshake Keys Calc\" step\n$ key=30a7e816f6a1e1b3434cf39cf4b415e7\n$ iv=11e70a5d1361795d2bb04465\n### from this record\n$ recdata=e00000000105735f63696405635f636964401600\n$ authtag=12512d7eda141ec057b804d30feb515b\n$ recordnum=0\n### may need to add -I and -L flags for include and lib dirs\n$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto\n$ cat /tmp/msg1 | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag | hexdump -C\n\n00000000 02 00 20 00 00 |.. ..|"
}
}
]
]
]
}
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"ACK 帧",
{
"props": {
"className": "bytes encrypted"
},
"content": "02 00 20 00 00"
},
[
"客户端确认收到服务器端的握手数据包0。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "02 - 表明帧类型 ACK"
},
{
"Tag": "li",
"content": "00 - largest_acknowledged: 被确认的最大数据包"
},
{
"Tag": "li",
"children": [
"20 - ack_delay: 变长的整数。给出这个 ack 被延迟发送的时间,单位是微秒。",
"通过计算式子:2",
{
"Tag": "sup",
"content": "ack_delay_exponent"
},
" 得到。其中 ack_delay_exponent = 32 * 8 = 256 (µseconds)。"
]
},
{
"Tag": "li",
"content": "00 - ack_range_count: 额外的 ACK 帧数据长度 0x(0)"
},
{
"Tag": "li",
"content": "00 - first_ack_range: 可变长度的整数。给出在 largest_acknowledged 之前被确认过的数据包数量。"
}
]
}
]
]
]
}
}
]
================================================
FILE: src/QUIC/clientHandshake2.json
================================================
[
"紧跟着,客户端需要再发送一个用于 \"握手\" 的数据包,包含了 \"握手完成\"的 TLS 记录,用于完成 TLS 1.3 加密会话的握手过程。",
{
"Tag": "AnnotationToggler"
},
{
"Tag": "Annotations",
"props": {
"type": "record-data",
"data": [
[
"数据包头字节",
{
"props": {
"className": "bytes protected",
"title": "被数据包头保护密钥加密",
"key": "pk"
},
"content": "e0"
},
{
"props": {
"className": "bytes unprotected"
},
"content": "e0"
},
[
"数据包以一个头字节开始,该字节应用了头保护。头部保护用于隐藏数据包序号和其他信息,使其不被外界观察到。",
"包头保护是通过用\"数据包头保护密钥\"对每个数据包的有效载荷的样本进行加密,然后将每个数据包中的某些比特和字节与所得数据进行异或(XOR)操作得到的。对于像这样的\"长\"格式数据包,受保护的部分是这个字节的低 4 位,以及数据包序号的字节(见下文)。",
"这里有一个关于如何计算出加密头字节的例子:",
{
"Tag": "CodeSample",
"props": {
"code": "### \"client header protection key\" from handshake keys calc step above\n$ key=84b3c21cacaf9f54c885e9a506459079\n### sample is taken from 16 bytes of payload starting\n### 4 bytes past the first byte of the packet number\n$ sample=9da7e61daa07732aa10b5fbd11a00a62\n$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p\n\nb0b3b06690\n\n### first byte of result is xor'd into lower 4 bits of this byte,\n### remaining bytes are xor'd one-for-one into the bytes of\n### the packet number (which in this packet is only one byte)"
}
},
"解密出的字节 0xE0 中的位有以下含义:",
{
"Tag": "Table",
"props": {
"headers": ["", "值", "含义"],
"data": [
["高位", "1", "Long Header 格式"],
["", "1", "固定位(总是被置1)"],
["", "10", "数据包类型:握手"],
["", "00", "保留(总是被置0)"],
[
"低位",
"00",
"数据包序号长度(表示下面的 \"数据包序号\" 将有一个字节的长度,默认值)"
]
]
}
}
]
],
[
"QUIC 版本号",
{
"props": {
"className": "bytes"
},
"content": "00 00 00 01"
},
["QUIC的版本是:0x1"]
],
[
"目的地连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 73 5f 63 69 64"
},
[
"服务器的标识 ID (服务器端的源连接标识 ID)",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的目的地连接标识 ID 的长度"
},
{
"Tag": "li",
"content": "73 5f 63 69 64 - 实际的目的地连接标识 ID(\"s_cid\")"
}
]
}
]
],
[
"源连接标识 ID",
{
"props": {
"className": "bytes"
},
"content": "05 63 5f 63 69 64"
},
[
"客户端使用这个字段来表明它选择的传输给服务器的源连接标识 ID。",
{
"Tag": "ul",
"children": [
{
"Tag": "li",
"content": "05 - 紧跟着的源连接标识 ID 的长度(5bytes)"
},
{
"Tag": "li",
"content": "63 5f 63 69 64 - 实际的源连接标识 ID (\"c_cid\")"
}
]
}
]
],
[
"数据包长度",
{
"props": {
"className": "bytes"
},
"content": "40 3f"
},
[
"客户端指明后续数据包数据的长度。这个字段是一个长度可变的整数——第一个字节的前两位表示该整数中总共有多少个字节。",
"此时,第一个字节以 \"0 1\"(0x4)这两个位开始,表示该整数共两个字节。其余的位给出数字 0x3f,表示 63 个字节的有效载荷。"
]
],
[
"
gitextract_b3_7o8a6/
├── .github/
│ └── workflows/
│ └── gh-pages.yml
├── .gitignore
├── .npmrc
├── .parcelrc
├── .prettierrc
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── frombootstrap.css
├── index.html
├── package.json
├── print.js
├── printmode.css
├── public/
│ ├── .nojekyll
│ ├── 404.html
│ └── sitemap.xml
└── src/
├── App.jsx
├── DTLS/
│ ├── clientApplicationDataDatagram.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientHandshakeFinishedDatagram.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientHelloDatagram.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverACKDatagram.json
│ ├── serverAlertDatagram.json
│ ├── serverApplicationDataDatagram.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverCertVerifyDatagram.json
│ ├── serverCertificateDatagram.json
│ ├── serverEncryptedExtensionsDatagram.json
│ ├── serverHandshakeFinishedDatagram.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHelloDatagram.json
│ └── serverKeyExchangeGeneration.json
├── Datagram/
│ ├── index.jsx
│ └── style.module.css
├── Footer/
│ └── index.jsx
├── Header/
│ ├── index.jsx
│ └── style.module.css
├── Intro/
│ ├── index.jsx
│ └── style.module.css
├── QUIC/
│ ├── clientAck.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientApplicationPacket1.json
│ ├── clientApplicationPacket2.json
│ ├── clientHandshake1.json
│ ├── clientHandshake2.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientInitialKeysCalc.json
│ ├── clientInitialPacket.json
│ ├── clientKeyExchangeGeneration.json
│ ├── clientTLSHandshakeFinished.json
│ ├── index.js
│ ├── padding.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverApplicationPacket1.json
│ ├── serverApplicationPacket2.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHandshakePacket1.json
│ ├── serverHandshakePacket2.json
│ ├── serverHandshakePacket3.json
│ ├── serverInitialKeysCalc.json
│ ├── serverInitialPacket.json
│ ├── serverKeyExchangeGeneration.json
│ ├── serverTLSHandshakeFinished.json
│ ├── tls-certificate.json
│ ├── tls-certificateVerify.json
│ ├── tls-clientHello.json
│ ├── tls-encryptedExtensions.json
│ └── tls-serverHello.json
├── RecOuter/
│ ├── Annotations/
│ │ ├── context.js
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── CodeSample/
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── Math/
│ │ ├── index.jsx
│ │ └── style.module.css
│ ├── Table/
│ │ └── index.jsx
│ ├── X25519/
│ │ ├── Calculator/
│ │ │ ├── PublicKeyMultiplier.jsx
│ │ │ ├── SecretKeyMultiplier.jsx
│ │ │ ├── YCoordinate.jsx
│ │ │ ├── index.jsx
│ │ │ ├── style.module.css
│ │ │ └── utils.js
│ │ ├── Flex/
│ │ │ ├── index.jsx
│ │ │ └── style.module.css
│ │ ├── QA/
│ │ │ ├── index.jsx
│ │ │ └── style.module.css
│ │ ├── curve.js
│ │ ├── field.js
│ │ └── index.js
│ ├── index.jsx
│ ├── style.module.css
│ └── utils.jsx
├── TLS12/
│ ├── clientApplicationData.json
│ ├── clientChangeCipherSpec.json
│ ├── clientCloseNotify.json
│ ├── clientEncryptionKeysGeneration.json
│ ├── clientHandshakeFinished.json
│ ├── clientHello.json
│ ├── clientKeyExchange.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverApplicationData.json
│ ├── serverCertificate.json
│ ├── serverChangeCipherSpec.json
│ ├── serverEncryptionKeysGeneration.json
│ ├── serverHandshakeFinished.json
│ ├── serverHello.json
│ ├── serverHelloDone.json
│ ├── serverKeyExchange.json
│ └── serverKeyExchangeGeneration.json
├── TLS13/
│ ├── clientApplicationData.json
│ ├── clientApplicationKeysCalc.json
│ ├── clientChangeCipherSpec.json
│ ├── clientHandshakeFinished.json
│ ├── clientHandshakeKeysCalc.json
│ ├── clientHello.json
│ ├── clientKeyExchangeGeneration.json
│ ├── index.js
│ ├── serverApplicationData.json
│ ├── serverApplicationKeysCalc.json
│ ├── serverCertVerify.json
│ ├── serverCertificate.json
│ ├── serverChangeCipherSpec.json
│ ├── serverEncryptedExtensions.json
│ ├── serverHandshakeFinished.json
│ ├── serverHandshakeKeysCalc.json
│ ├── serverHello.json
│ ├── serverKeyExchangeGeneration.json
│ ├── serverNewSessionTicket1.json
│ ├── serverNewSessionTicket2.json
│ ├── wrappedRecord1.json
│ ├── wrappedRecord2.json
│ ├── wrappedRecord3.json
│ ├── wrappedRecord4.json
│ ├── wrappedRecord5.json
│ ├── wrappedRecord6.json
│ ├── wrappedRecord7.json
│ ├── wrappedRecord8.json
│ └── wrappedRecord9.json
├── X25519/
│ ├── handsOn.json
│ ├── index.js
│ ├── mathOnTheCurve.json
│ ├── overview.json
│ └── q&a.json
├── common.css
├── context/
│ └── slugger.js
├── hard-encoded.css
├── illustrated.css
├── index.js
├── router.js
└── utils.js
SYMBOL INDEX (4 symbols across 3 files)
FILE: src/RecOuter/X25519/Calculator/utils.js
function getHexValue (line 5) | function getHexValue(raw = "", def) {
function setHexValue (line 22) | function setHexValue(callback = () => {}) {
FILE: src/RecOuter/X25519/curve.js
function curve (line 3) | function curve(basePointX = 9n) {
FILE: src/RecOuter/X25519/field.js
function field (line 2) | function field(p = 2n ** 255n - 19n) {
Condensed preview — 154 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (828K chars).
[
{
"path": ".github/workflows/gh-pages.yml",
"chars": 1354,
"preview": "name: deploy gh-pages\n\non:\n pull_request:\n types: [closed]\n branches: [master]\n\n push:\n branches: [master]\n\nj"
},
{
"path": ".gitignore",
"chars": 77,
"preview": "build\ndist\nnode_modules\n\n.parcel-cache\n\n*.local\n\nyarn.lock\npackage-lock.json\n"
},
{
"path": ".npmrc",
"chars": 43,
"preview": "registry = https://registry.npmmirror.com/\n"
},
{
"path": ".parcelrc",
"chars": 125,
"preview": "{\n \"extends\": [\n \"@parcel/config-default\"\n ],\n \"reporters\": [\n \"...\",\n \"parcel-reporter-static-files-copy\"\n "
},
{
"path": ".prettierrc",
"chars": 107,
"preview": "{\n \"trailingComma\": \"es5\",\n \"singleQuote\": false,\n \"tabWidth\": 2,\n \"semi\": true,\n \"endOfLine\": \"lf\"\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 67,
"preview": "{\n \"cSpell.words\": [\"middleboxes\", \"middlebox\", \"DTLS\", \"QUIC\"]\n}\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2023 Allen Lee\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 401,
"preview": "# illustrate QUIC, TLS 1.2, TLS 1.3, DTLS 中文翻译\n\n图解 QUIC, TLS 1.2, TLS 1.3, DTLS 协议的连接及会话过程\n\n翻译自:\n\n- https://quic.xargs.o"
},
{
"path": "frombootstrap.css",
"chars": 3251,
"preview": "/* everything we wanted from bootstrap but nothing more */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {"
},
{
"path": "index.html",
"chars": 1950,
"preview": "<!DOCTYPE html>\n<html lang=\"cn\" dir=\"ltr\">\n\n<head>\n <script type=\"text/javascript\">\n // Single Page Apps for GitHub "
},
{
"path": "package.json",
"chars": 787,
"preview": "{\n \"name\": \"illustrate\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"scripts\": {\n \"start\": \"parcel index.html\",\n "
},
{
"path": "print.js",
"chars": 691,
"preview": "globalThis.illustrate = {\n printMode: () => {\n // add printmode css\n let inject = document.createElement(\"link\");"
},
{
"path": "printmode.css",
"chars": 1031,
"preview": "/* print mode */\n\n@media (min-width: 0) {\n .container {\n max-width: none !important;\n margin: 5px 0 !important;\n "
},
{
"path": "public/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "public/404.html",
"chars": 1796,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\">\n <title>Single Page Apps for GitHub Pages</title>\n <scri"
},
{
"path": "public/sitemap.xml",
"chars": 1135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:news=\"http:"
},
{
"path": "src/App.jsx",
"chars": 2458,
"preview": "import React from \"react\";\n\nimport \"./common.css\";\nimport \"./hard-encoded.css\";\nimport \"./illustrated.css\";\n\nimport Head"
},
{
"path": "src/DTLS/clientApplicationDataDatagram.json",
"chars": 6112,
"preview": "[\n \"客户端发送数据:字符串\\\"ping\\\"\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n \"t"
},
{
"path": "src/DTLS/clientApplicationKeysCalc.json",
"chars": 1300,
"preview": "[\n \"客户端现在也可以计算应用会话时的密钥了。如果计算正确,结果应和服务器端的一致:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/DTLS/clientHandshakeFinishedDatagram.json",
"chars": 10032,
"preview": "[\n \"为了验证握手成功且没有被篡改过,客户端和服务器端一样,需要创建一些验证数据给服务器端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n "
},
{
"path": "src/DTLS/clientHandshakeKeysCalc.json",
"chars": 2647,
"preview": "[\n \"客户端现在也拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,客户端使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\","
},
{
"path": "src/DTLS/clientHelloDatagram.json",
"chars": 15055,
"preview": "[\n \"DTLS 加密会话以 \\\"ClientHello\\\" 开始。客户端提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\","
},
{
"path": "src/DTLS/clientKeyExchangeGeneration.json",
"chars": 1646,
"preview": "[\n \"连接开始时,客户端生成一个用于密钥交换的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/DTLS/index.js",
"chars": 4443,
"preview": "const data = {\n intro: {\n title: \"图解 DTLS 连接\",\n subtitle: \"对每一个字节的解释和再现\",\n desc: 'DTLS 应被称为 \"通过数据报传输的TLS\";到目前为"
},
{
"path": "src/DTLS/serverACKDatagram.json",
"chars": 7255,
"preview": "[\n \"每个对等端(peer)必须响应或确认从其他对等端收到的数据,否则对应对等端将假定数据已丢失并会再次发送。\",\n \"因此,在这个记录中,服务器端需要确认收到了客户端握手完成的记录。\",\n {\n \"Tag\": \"Annota"
},
{
"path": "src/DTLS/serverAlertDatagram.json",
"chars": 6490,
"preview": "[\n \"服务器端发送警告信号,表明连接即将终止(此时是有序终止即正常终止连接)\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"pr"
},
{
"path": "src/DTLS/serverApplicationDataDatagram.json",
"chars": 6116,
"preview": "[\n \"服务器端响应数据:字符串\\\"pong\\\"\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n \""
},
{
"path": "src/DTLS/serverApplicationKeysCalc.json",
"chars": 5390,
"preview": "[\n \"服务器端现在可以计算应用会话时的密钥了。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/DTLS/serverCertVerifyDatagram.json",
"chars": 12958,
"preview": "[\n \"服务器提供证书验证相关的数据,用以验证服务器密钥交换生成过程中产生的短暂公钥与证书私钥的所有权是否一致。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Anno"
},
{
"path": "src/DTLS/serverCertificateDatagram.json",
"chars": 16277,
"preview": "[\n \"服务器会发送一个或多个证书:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n \"content\": \"该主机的证书,包含主"
},
{
"path": "src/DTLS/serverEncryptedExtensionsDatagram.json",
"chars": 9643,
"preview": "[\n \"连接(包括握手)的数据从这时候起就能够被加密了。加密握手数据是 DTLS 1.3 的新特性。\",\n \"任何不需要协商其他加密密钥的扩展都会列在这里。加密以隐藏它们不被窃听者和中间件(middleboxes)发现。\",\n {\n "
},
{
"path": "src/DTLS/serverHandshakeFinishedDatagram.json",
"chars": 9965,
"preview": "[\n \"为了验证握手成功且没有被篡改过,服务器会创建一些验证数据给客户端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": "
},
{
"path": "src/DTLS/serverHandshakeKeysCalc.json",
"chars": 6464,
"preview": "[\n \"服务器现在拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n"
},
{
"path": "src/DTLS/serverHelloDatagram.json",
"chars": 7671,
"preview": "[\n \"服务器回复 \\\"ServerHello\\\"。服务器提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/DTLS/serverKeyExchangeGeneration.json",
"chars": 1552,
"preview": "[\n \"服务器端也需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"c"
},
{
"path": "src/Datagram/index.jsx",
"chars": 290,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\n\nconst Datagram = ({ label = \"\", children }) => {\n "
},
{
"path": "src/Datagram/style.module.css",
"chars": 224,
"preview": "/***** datagram borders *****/\n.datagram {\n max-width: 800px;\n border-radius: 1.2em;\n border: 1px solid black;\n padd"
},
{
"path": "src/Footer/index.jsx",
"chars": 1513,
"preview": "import React from \"react\";\n\nconst Footer = ({ desc, mother, references }) => {\n return (\n <>\n <div>\n <p\n"
},
{
"path": "src/Header/index.jsx",
"chars": 1834,
"preview": "import React from \"react\";\nimport clsx from \"clsx\";\nimport classes from \"./style.module.css\";\nimport { jump } from \"../u"
},
{
"path": "src/Header/style.module.css",
"chars": 494,
"preview": ".header {\n width: 100%;\n margin: 0;\n padding: 2px;\n border-bottom: 1px solid grey;\n background-color: bisque;\n col"
},
{
"path": "src/Intro/index.jsx",
"chars": 643,
"preview": "import classes from \"./style.module.css\";\nimport Balancer from \"react-wrap-balancer\";\n\nconst Intro = ({ title = \"\", subt"
},
{
"path": "src/Intro/style.module.css",
"chars": 317,
"preview": "h1:after {\n content: \"\\00a0❧\";\n}\nh1:before {\n content: \"❧\\00a0\";\n}\nh1,\nh3,\nh5 {\n text-align: center;\n padding: 10px;"
},
{
"path": "src/QUIC/clientAck.json",
"chars": 9549,
"preview": "[\n \"客户端再发送一个 \\\"初始\\\" 的数据包。其中包含服务器的最后一个 \\\"初始\\\" 数据包的 ACK。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annota"
},
{
"path": "src/QUIC/clientApplicationKeysCalc.json",
"chars": 1308,
"preview": "[\n \"客户端现在也可以计算应用会话时的密钥了。如果计算正确,结果应和服务器端的一致:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/QUIC/clientApplicationPacket1.json",
"chars": 9098,
"preview": "[\n \"客户端发送其第一个握手后的数据包,即第一个实际包含应用内容的会话过程数据包。其中包含内容为 \\\"ping\\\" 的流数据。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag"
},
{
"path": "src/QUIC/clientApplicationPacket2.json",
"chars": 8006,
"preview": "[\n \"客户端接收后,需要发送一个用于告知服务器已收到数据的 \\\"ACK\\\" 数据包。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n "
},
{
"path": "src/QUIC/clientHandshake1.json",
"chars": 9062,
"preview": "[\n \"紧跟着,客户端需要发送一个用于 \\\"握手\\\" 的数据包。这个数据包包含对服务器端 \\\"握手\\\" 数据包的 ACK。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": "
},
{
"path": "src/QUIC/clientHandshake2.json",
"chars": 10879,
"preview": "[\n \"紧跟着,客户端需要再发送一个用于 \\\"握手\\\" 的数据包,包含了 \\\"握手完成\\\"的 TLS 记录,用于完成 TLS 1.3 加密会话的握手过程。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },"
},
{
"path": "src/QUIC/clientHandshakeKeysCalc.json",
"chars": 2640,
"preview": "[\n \"客户端现在拥有了用于计算剩余握手步骤的加密密钥的所有信息。在这个计算中,客户端使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li"
},
{
"path": "src/QUIC/clientInitialKeysCalc.json",
"chars": 5277,
"preview": "[\n \"接下来,客户端继续为连接做准备:生成初始数据包(packet)的加密密钥。由于客户端和服务器之间还没有进行任何数据传输,因此这些密钥的安全性有限——任何观察者都可以得出密钥,并像服务器一样读取流量。加密初始数据包可以防止某些类型的"
},
{
"path": "src/QUIC/clientInitialPacket.json",
"chars": 10650,
"preview": "[\n \"会话开始时,客户端发送一个初始数据包。这个数据包包含 \\\"ClientHello\\\" TLS 记录,用于开始 TLS 1.3 加密会话。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n"
},
{
"path": "src/QUIC/clientKeyExchangeGeneration.json",
"chars": 1646,
"preview": "[\n \"连接开始时,客户端生成一个用于密钥交换的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/QUIC/clientTLSHandshakeFinished.json",
"chars": 2670,
"preview": "[\n \"为了验证握手成功且没有被篡改过,客户端和服务器端一样,需要创建一些验证数据给服务器端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n "
},
{
"path": "src/QUIC/index.js",
"chars": 7154,
"preview": "const data = {\n intro: {\n title: \"图解 QUIC 连接\",\n subtitle: \"对每一个字节的解释和再现\",\n desc: \"QUIC 是一个基于 UDP 的安全流协议,构成了 HT"
},
{
"path": "src/QUIC/padding.json",
"chars": 1886,
"preview": "[\n \"客户端发送的任何包含初始数据包的数据报必须将包填充/对齐到 1200 字节的长度。在数据报上添加 nul 字节(填0)即可。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"T"
},
{
"path": "src/QUIC/serverApplicationKeysCalc.json",
"chars": 5071,
"preview": "[\n \"服务器端现在可以计算应用会话时的密钥了。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/QUIC/serverApplicationPacket1.json",
"chars": 10943,
"preview": "[\n \"服务器端发送其第一个握手后的数据包,即第一个实际包含应用内容的会话过程数据包。其中包含内容为 \\\"pong\\\" 的流数据响应。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \""
},
{
"path": "src/QUIC/serverApplicationPacket2.json",
"chars": 7848,
"preview": "[\n \"服务器在确认客户端已收到所有待处理数据后,关闭连接。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n "
},
{
"path": "src/QUIC/serverHandshakeKeysCalc.json",
"chars": 6025,
"preview": "[\n \"服务器现在拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n"
},
{
"path": "src/QUIC/serverHandshakePacket1.json",
"chars": 15749,
"preview": "[\n \"紧跟着,服务器需要发送一个用于 \\\"握手\\\" 的数据包。这个数据包包含 TLS 需求的握手记录,用于继续 TLS 1.3 加密会话的协商。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {"
},
{
"path": "src/QUIC/serverHandshakePacket2.json",
"chars": 10535,
"preview": "[\n \"服务器需要再发送另一个 \\\"握手\\\" 数据包,用于继续 TLS 1.3 加密会话的握手过程。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotation"
},
{
"path": "src/QUIC/serverHandshakePacket3.json",
"chars": 9073,
"preview": "[\n \"服务器端接收后,需要发送一个用于 \\\"握手 ACK\\\" 的数据包,用于完成 TLS 1.3 加密会话的握手过程。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \""
},
{
"path": "src/QUIC/serverInitialKeysCalc.json",
"chars": 1601,
"preview": "[\n \"接下来,服务器需要对自己的初始密钥进行计算。它从客户端的第一个初始数据包的 \\\"目的地连接标识 ID\\\" 字段中获得8字节的随机数据:\",\n {\n \"Tag\": \"pre\",\n \"props\": {\n \"c"
},
{
"path": "src/QUIC/serverInitialPacket.json",
"chars": 11538,
"preview": "[\n \"服务器以一个 \\\"初始\\\" 数据包作为回应。这个数据包包含 \\\"ServerHello\\\" 的 TLS 记录,用于继续 TLS 1.3 加密会话的协商。\",\n {\n \"Tag\": \"AnnotationToggler\"\n "
},
{
"path": "src/QUIC/serverKeyExchangeGeneration.json",
"chars": 1552,
"preview": "[\n \"服务器端也需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"c"
},
{
"path": "src/QUIC/serverTLSHandshakeFinished.json",
"chars": 2648,
"preview": "[\n \"为了验证握手成功且没有被篡改过,服务器会创建一些验证数据给客户端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": "
},
{
"path": "src/QUIC/tls-certificate.json",
"chars": 6341,
"preview": "[\n \"服务器会发送一个或多个证书:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n \"content\": \"该主机的证书,包含主"
},
{
"path": "src/QUIC/tls-certificateVerify.json",
"chars": 4790,
"preview": "[\n \"服务器提供证书验证相关的数据,用以验证服务器密钥交换生成过程中产生的短暂公钥与证书私钥的所有权是否一致。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Anno"
},
{
"path": "src/QUIC/tls-clientHello.json",
"chars": 19910,
"preview": "[\n \"加密会话以 TLS 的 \\\"ClientHello\\\" 开始。客户端提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\""
},
{
"path": "src/QUIC/tls-encryptedExtensions.json",
"chars": 8785,
"preview": "[\n \"任何不需要协商更多加密密钥的扩展都会列在这里。加密以隐藏它们不被窃听者和中间件(middleboxes)发现。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"A"
},
{
"path": "src/QUIC/tls-serverHello.json",
"chars": 5572,
"preview": "[\n \"服务器回复 \\\"ServerHello\\\"。服务器提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/RecOuter/Annotations/context.js",
"chars": 459,
"preview": "import React from \"react\";\n\nconst AnnotationContext = React.createContext();\n\nexport const AnnotationContextProvider = ("
},
{
"path": "src/RecOuter/Annotations/index.jsx",
"chars": 1700,
"preview": "import React from \"react\";\nimport clsx from \"clsx\";\nimport classes from \"./style.module.css\";\nimport { renderExplanation"
},
{
"path": "src/RecOuter/Annotations/style.module.css",
"chars": 2390,
"preview": ".annotate-toggle {\n margin-bottom: 1em;\n\n display: inline-block;\n outline: none;\n cursor: pointer;\n text-align: cen"
},
{
"path": "src/RecOuter/CodeSample/index.jsx",
"chars": 495,
"preview": "import clsx from \"clsx\";\nimport React from \"react\";\nimport classes from \"./style.module.css\";\n\nconst CodeSample = ({ cod"
},
{
"path": "src/RecOuter/CodeSample/style.module.css",
"chars": 1270,
"preview": "/***** .sample *****/\n\n.sample {\n display: block;\n margin: 1em 0;\n position: relative;\n}\n\n.sample pre {\n margin: 0;\n"
},
{
"path": "src/RecOuter/Math/index.jsx",
"chars": 3734,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\n\nconst Types = {\n Normal: 0,\n NumericalLeading: 1"
},
{
"path": "src/RecOuter/Math/style.module.css",
"chars": 136,
"preview": ".math {\n font-family: \"Times New Roman\", serif;\n font-style: oblique;\n}\n.math sup {\n top: -0.5em;\n}\n.math sub {\n bot"
},
{
"path": "src/RecOuter/Table/index.jsx",
"chars": 1013,
"preview": "import React from \"react\";\nimport { renderExplanations } from \"../utils\";\n\nconst Table = ({ headers, data, dataProps = ["
},
{
"path": "src/RecOuter/X25519/Calculator/PublicKeyMultiplier.jsx",
"chars": 1856,
"preview": "import React from \"react\";\nimport { useHexValue, calcMulti, getHexValue } from \"./utils\";\n\nconst PublicKeyMultiplier = ("
},
{
"path": "src/RecOuter/X25519/Calculator/SecretKeyMultiplier.jsx",
"chars": 2945,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\nimport { useHexValue, calcPubkey, getHexValue } fro"
},
{
"path": "src/RecOuter/X25519/Calculator/YCoordinate.jsx",
"chars": 1485,
"preview": "import React, { useState } from \"react\";\nimport { useHexValue, calcY, getHexValue } from \"./utils\";\nimport MathBlock fro"
},
{
"path": "src/RecOuter/X25519/Calculator/index.jsx",
"chars": 759,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\nimport { startCase } from \"./utils\";\nimport SecretK"
},
{
"path": "src/RecOuter/X25519/Calculator/style.module.css",
"chars": 2387,
"preview": ".calculator {\n max-width: 48em;\n display: block;\n font-size: 14px;\n margin: 1em auto;\n padding: 1em;\n background-c"
},
{
"path": "src/RecOuter/X25519/Calculator/utils.js",
"chars": 2168,
"preview": "import React from \"react\";\nimport field from \"../field\";\nimport curve from \"../curve\";\n\nexport function getHexValue(raw "
},
{
"path": "src/RecOuter/X25519/Flex/index.jsx",
"chars": 442,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\nimport { renderExplanations } from \"../../utils\";\n\n"
},
{
"path": "src/RecOuter/X25519/Flex/style.module.css",
"chars": 70,
"preview": ".container {\n display: flex;\n width: 100%;\n flex-direction: row;\n}\n"
},
{
"path": "src/RecOuter/X25519/QA/index.jsx",
"chars": 707,
"preview": "import React from \"react\";\nimport classes from \"./style.module.css\";\nimport { renderExplanations } from \"../../utils\";\n\n"
},
{
"path": "src/RecOuter/X25519/QA/style.module.css",
"chars": 176,
"preview": ".qa-mark {\n margin-top: 12px;\n margin-right: 4px;\n display: inline-block;\n font-family: \"Baskerville\", sans-serif;\n "
},
{
"path": "src/RecOuter/X25519/curve.js",
"chars": 4511,
"preview": "import field from './field';\n\nfunction curve(basePointX = 9n) {\n const curveA = 486662n;\n\n /**\n * Given intermediate"
},
{
"path": "src/RecOuter/X25519/field.js",
"chars": 5143,
"preview": "// duplication from: https://github.com/syncsynchalt/illustrated-x25519/tree/main/js\nfunction field(p = 2n ** 255n - 19n"
},
{
"path": "src/RecOuter/X25519/index.js",
"chars": 143,
"preview": "export { default as Calculator } from \"./Calculator\";\nexport { default as FlexContainer } from \"./Flex\";\nexport { defaul"
},
{
"path": "src/RecOuter/index.jsx",
"chars": 3470,
"preview": "import clsx from \"clsx\";\nimport React from \"react\";\nimport classes from \"./style.module.css\";\nimport { AnnotationContext"
},
{
"path": "src/RecOuter/style.module.css",
"chars": 2585,
"preview": ".rec-outer {\n max-width: 800px;\n margin: 0.8em auto;\n position: relative;\n}\n\n.server {\n background-color: var(--serv"
},
{
"path": "src/RecOuter/utils.jsx",
"chars": 2408,
"preview": "import CodeSample from \"./CodeSample\";\nimport Annotations, { AnnotationToggler } from \"./Annotations\";\nimport Table from"
},
{
"path": "src/TLS12/clientApplicationData.json",
"chars": 3587,
"preview": "[\n \"此时客户端和服务器就可以开始正式的 TLS 1.2 会话了。\",\n \"客户端首先发送它的数据,字符串\\\"ping\\\"。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag"
},
{
"path": "src/TLS12/clientChangeCipherSpec.json",
"chars": 1281,
"preview": "[\n \"生成完主秘钥之后,客户端会发送一个密钥规格变更记录(Change Cipher Spec),表示已经生成主秘钥,并且将模式切换到加密模式,告诉服务器端开始使用加密方式发送消息。密钥规格变更记录之前传输的 TLS 握手数据都是明文的"
},
{
"path": "src/TLS12/clientCloseNotify.json",
"chars": 3958,
"preview": "[\n \"客户端发送警告信号,表明连接即将终止(此时是有序终止即正常终止连接)\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"pro"
},
{
"path": "src/TLS12/clientEncryptionKeysGeneration.json",
"chars": 8408,
"preview": "[\n \"客户端现在可以计算应用会话时的密钥了。在这个计算中,客户端使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS12/clientHandshakeFinished.json",
"chars": 6440,
"preview": "[\n \"为了验证握手成功且没有被篡改过,客户端需要创建一些验证数据给服务器端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n \"此时开始传输的客户端数据都是加密的密文了。\",\n {\n \"Tag\": \"Annotation"
},
{
"path": "src/TLS12/clientHello.json",
"chars": 16414,
"preview": "[\n \"例子中的会话以 \\\"ClientHello\\\" 开始。客户端提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS12/clientKeyExchange.json",
"chars": 2458,
"preview": "[\n \"客户端现在可以算出自己的密钥。交换后,双方都可以根据私钥和交换过来的公钥生成共享的加密密钥了。\",\n \"现在双方已经同意使用 ECDHE 的密码加解密算法(ServiceHello 的 TLS_ECDHE_RSA_WITH_AE"
},
{
"path": "src/TLS12/clientKeyExchangeGeneration.json",
"chars": 1550,
"preview": "[\n \"客户端也需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/TLS12/index.js",
"chars": 4407,
"preview": "const data = {\n intro: {\n title: \"图解 TLS 1.2 连接\",\n subtitle: \"对每一个字节的解释和再现\",\n desc: \"TLS 1.3 已于 2018/08 释出。\",\n"
},
{
"path": "src/TLS12/serverApplicationData.json",
"chars": 3557,
"preview": "[\n \"服务器端响应客户端的数据,返回字符串\\\"pong\\\"。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n"
},
{
"path": "src/TLS12/serverCertificate.json",
"chars": 6169,
"preview": "[\n \"服务器端发送的证书包含以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n \"content\": \"服务器端的主机名"
},
{
"path": "src/TLS12/serverChangeCipherSpec.json",
"chars": 1282,
"preview": "[\n \"生成完主秘钥之后,服务器端也需要发送一个密钥规格变更记录(Change Cipher Spec),表示已经生成主秘钥,并且将模式切换到加密模式,告诉客户端开始使用加密方式发送消息。密钥规格变更记录之前传输的 TLS 握手数据都是明"
},
{
"path": "src/TLS12/serverEncryptionKeysGeneration.json",
"chars": 8426,
"preview": "[\n \"服务器端现在也可以计算应用会话时的密钥了。在这个计算中,服务器端使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS12/serverHandshakeFinished.json",
"chars": 6446,
"preview": "[\n \"为了验证握手成功且没有被篡改过,服务器端也需要创建一些验证数据给客户端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n \"此时开始传输的服务器端数据都是加密的密文了。\",\n {\n \"Tag\": \"Annotati"
},
{
"path": "src/TLS12/serverHello.json",
"chars": 6201,
"preview": "[\n \"服务器回复 \\\"ServerHello\\\"。服务器提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS12/serverHelloDone.json",
"chars": 1562,
"preview": "[\n \"服务器表示它已经完成了它那一半的握手过程。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n "
},
{
"path": "src/TLS12/serverKeyExchange.json",
"chars": 6480,
"preview": "[\n \"服务器将密钥交换的公钥发送给客户端。等客户端算出自己的密钥交换后,共享的加密密钥就可以根据私钥和交换过来的公钥生成了。\",\n \"现在双方已经同意使用 ECDHE 的密码加解密算法(ServiceHello 的 TLS_ECDHE"
},
{
"path": "src/TLS12/serverKeyExchangeGeneration.json",
"chars": 1550,
"preview": "[\n \"服务器端需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/TLS13/clientApplicationData.json",
"chars": 374,
"preview": "[\n \"客户端发送数据:字符串\\\"ping\\\"\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n \"t"
},
{
"path": "src/TLS13/clientApplicationKeysCalc.json",
"chars": 955,
"preview": "[\n \"客户端现在也可以计算应用会话时的密钥了。如果计算正确,结果应和服务器端的一致:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS13/clientChangeCipherSpec.json",
"chars": 1199,
"preview": "[\n \"这条记录是用于旧版本 TLS 的。TLS 1.3 虽然不会产生效果,但是在开启\\\"中间件兼容模式\\\"时,该记录仍然会被发送,以帮助将 TLS 1.3 的会话伪装成 TLS 1.2 的会话。\",\n {\n \"Tag\": \"An"
},
{
"path": "src/TLS13/clientHandshakeFinished.json",
"chars": 2998,
"preview": "[\n \"为了验证握手成功且没有被篡改过,客户端和服务器端一样,需要创建一些验证数据给服务器端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n "
},
{
"path": "src/TLS13/clientHandshakeKeysCalc.json",
"chars": 2517,
"preview": "[\n \"客户端现在也拥有了用于计算剩余握手步骤的加密密钥的所有信息。在这个计算中,客户端使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"l"
},
{
"path": "src/TLS13/clientHello.json",
"chars": 20717,
"preview": "[\n \"TLS 会话以 \\\"ClientHello\\\" 开始。客户端提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS13/clientKeyExchangeGeneration.json",
"chars": 1579,
"preview": "[\n \"连接开始时,客户端生成一个用于密钥交换的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/TLS13/index.js",
"chars": 7327,
"preview": "const data = {\n intro: {\n title: \"图解 TLS 1.3 连接\",\n subtitle: \"对每一个字节的解释和再现\",\n desc: '这里演示的连接是基于 OpenSSL 3.0.1 "
},
{
"path": "src/TLS13/serverApplicationData.json",
"chars": 375,
"preview": "[\n \"服务器端响应数据:字符串\\\"pong\\\"\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n \"props\": {\n \""
},
{
"path": "src/TLS13/serverApplicationKeysCalc.json",
"chars": 4963,
"preview": "[\n \"服务器端现在可以计算应用会话时的密钥了。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS13/serverCertVerify.json",
"chars": 4971,
"preview": "[\n \"服务器提供证书验证相关的数据,用以验证服务器密钥交换生成过程中产生的短暂公钥与证书私钥的所有权是否一致。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Anno"
},
{
"path": "src/TLS13/serverCertificate.json",
"chars": 6299,
"preview": "[\n \"服务器会发送一个或多个证书:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n \"content\": \"该主机的证书,包含主"
},
{
"path": "src/TLS13/serverChangeCipherSpec.json",
"chars": 1229,
"preview": "[\n \"这条记录是用于旧版本 TLS 的。TLS 1.3 虽然不会产生效果,但是在开启\\\"中间件兼容模式\\\"(middlebox compatibility mode)时,该记录仍然会被发送,以帮助将 TLS 1.3 的会话伪装成 TLS"
},
{
"path": "src/TLS13/serverEncryptedExtensions.json",
"chars": 1272,
"preview": "[\n \"任何不需要协商其他加密密钥的扩展都会列在这里,以便对窃听者和中间件隐藏它们。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": \"Annotations\",\n "
},
{
"path": "src/TLS13/serverHandshakeFinished.json",
"chars": 2950,
"preview": "[\n \"为了验证握手成功且没有被篡改过,服务器会创建一些验证数据给客户端确认。验证数据是基于所有握手信息的哈希值计算得到。\",\n {\n \"Tag\": \"AnnotationToggler\"\n },\n {\n \"Tag\": "
},
{
"path": "src/TLS13/serverHandshakeKeysCalc.json",
"chars": 7892,
"preview": "[\n \"服务器现在拥有了用于计算握手时的加密密钥的所有信息。在这个计算中,服务器使用了以下信息:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n"
},
{
"path": "src/TLS13/serverHello.json",
"chars": 6630,
"preview": "[\n \"服务器回复 \\\"ServerHello\\\"。服务器提供的信息包括以下内容:\",\n {\n \"Tag\": \"ul\",\n \"children\": [\n {\n \"Tag\": \"li\",\n "
},
{
"path": "src/TLS13/serverKeyExchangeGeneration.json",
"chars": 1550,
"preview": "[\n \"服务器端需要生成一个用于密钥交换的自己的“私钥/公钥”对。密钥交换(Key exchange)是一种技术,双方可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。\",\n {\n \"Tag\": \"p\",\n \"ch"
},
{
"path": "src/TLS13/serverNewSessionTicket1.json",
"chars": 4041,
"preview": "[\n \"服务器在协商末尾阶段会提供会话记录单,客户端可以在之后使用它以启动一个新的会话。以这种方式成功恢复连接将跳过会话启动中的大部分计算和网络延迟。\",\n \"因为每个会话记录单都是一次性的,而服务器希望对等端可以快速打开多个连接,所以"
},
{
"path": "src/TLS13/serverNewSessionTicket2.json",
"chars": 4041,
"preview": "[\n \"服务器在协商末尾阶段会提供会话记录单,客户端可以在之后使用它以启动一个新的会话。以这种方式成功恢复连接将跳过会话启动中的大部分计算和网络延迟。\",\n \"因为每个会话记录单都是一次性的,而服务器希望对等端可以快速打开多个连接,所以"
},
{
"path": "src/TLS13/wrappedRecord1.json",
"chars": 4680,
"preview": "[\n \"连接(包括握手)的数据从这时候起就能够被加密了。加密握手数据是 TLS 1.3 的新特性。\",\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1"
},
{
"path": "src/TLS13/wrappedRecord2.json",
"chars": 10163,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord3.json",
"chars": 6458,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord4.json",
"chars": 5281,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord5.json",
"chars": 5281,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord6.json",
"chars": 4605,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord7.json",
"chars": 6084,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord8.json",
"chars": 6075,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/TLS13/wrappedRecord9.json",
"chars": 4607,
"preview": "[\n \"为了减少连接被无法识别新 TLS 协议的中间件阻断的问题,加密的 TLS 1.3 记录都需要伪装成一个会话恢复成功的 TLS 1.2 会话记录(之所以不是伪装成握手记录,因为握手记录过长会使得兼容实现变得异常复杂)。\",\n \"包"
},
{
"path": "src/X25519/handsOn.json",
"chars": 2126,
"preview": "[\n \"让我们用一些真实的数字来试一试 X25519 算法。这个计算器允许我们基点 P 上做标量乘法,P 点为曲线上 x=9 的点。我们讨论的计算仅需要 nP 的 x 坐标,而 y 坐标不影响计算且并不需要使用。\",\n {\n \"T"
},
{
"path": "src/X25519/index.js",
"chars": 1077,
"preview": "const data = {\n intro: {\n title: \"图解 X25519 密钥交换算法\",\n subtitle: \"\",\n },\n sections: [\n {\n type: \"RecOute"
},
{
"path": "src/X25519/mathOnTheCurve.json",
"chars": 4246,
"preview": "[\n {\n \"children\": [\n \"这一节的内容是可选的。这一节仅大致描述了椭圆曲线的数学内容。对相关内容的深入的动画解释可以从\",\n {\n \"Tag\": \"a\",\n \"pro"
},
{
"path": "src/X25519/overview.json",
"chars": 1909,
"preview": "[\n \"密钥交换(Key exchange)是一种技术,双方(为方便叙述假设为 Alice 和 Bob)可以在同一数字上达成一致,而窃听者却无法知道这个数字是什么。X25519 是密钥交换其中一种方法的名称。具体一点是:通过做如下的 Cu"
},
{
"path": "src/X25519/q&a.json",
"chars": 7393,
"preview": "[\n {\n \"Tag\": \"QA\",\n \"props\": {\n \"content\": [\n [\"为什么我不能在 openssl 或其他工具中重现这些结果?\"],\n [\n "
},
{
"path": "src/common.css",
"chars": 1926,
"preview": ":root {\n --chunky-text: #777;\n --server-bg: hsl(190, 60%, 80%);\n --client-bg: hsl(142, 61%, 82%);\n --server-bg-hover"
},
{
"path": "src/context/slugger.js",
"chars": 928,
"preview": "import React from \"react\";\nimport { slug as githubSlug } from \"github-slugger\";\nimport { jump } from \"../utils\";\n\nconst "
},
{
"path": "src/hard-encoded.css",
"chars": 1047,
"preview": "code.longboi {\n word-break: break-all;\n}\n.ind1 {\n padding-left: 1em;\n}\n.ind2 {\n padding-left: 2em;\n}\n.decryption-head"
},
{
"path": "src/illustrated.css",
"chars": 1414,
"preview": "/***** processblock (not used in quic, delete?) *****/\n\nprocessblock {\n display: block;\n position: relative;\n margin:"
},
{
"path": "src/index.js",
"chars": 262,
"preview": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App\";\nimport { Provider } from \"react-wrap-balancer\";\n"
},
{
"path": "src/router.js",
"chars": 695,
"preview": "export const base = \"/illustrate\";\n\nexport const routers = [\n {\n label: \"QUIC\",\n href: \"/quic\",\n title: \"图解 QU"
},
{
"path": "src/utils.js",
"chars": 216,
"preview": "export const jump = (href) => {\n if (globalThis.location.pathname.startsWith(href)) return;\n\n globalThis.history.pushS"
}
]
About this extraction
This page contains the full source code of the cangSDARM/illustrate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 154 files (638.1 KB), approximately 226.3k tokens, and a symbol index with 4 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.