A place to cache linked articles (think custom and personal wayback machine)
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

index.html 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>HPKP: HTTP Public Key Pinning (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://scotthelme.co.uk/hpkp-http-public-key-pinning/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. HPKP: HTTP Public Key Pinning (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://scotthelme.co.uk/hpkp-http-public-key-pinning/">Source originale du contenu</a></h3>
  445. <p>HTTP Public Key Pinning, or HPKP, is a security policy delivered via a HTTP response header much like <a href="https://scotthelme.co.uk/hsts-the-missing-link-in-tls/">HSTS</a> and <a href="https://scotthelme.co.uk/content-security-policy-an-introduction/">CSP</a>. It allows a host to provide information to a user agent about which cryptographic identities it should accept from the host in the future. This can protect a host website from a security compromise at a Certificate Authority where rogue certificates may be issued for your hostname. </p>
  446. <p> </p>
  447. <h2 id="whatishpkp">What Is HPKP?</h2>
  448. <p>HPKP is a HTTP response header that allows a host to deliver a set of fingerprints to cryptographic identities that the User Agent (UA) should accept for the site going forwards. Much like HSTS, HPKP is a Trust On First Use (TOFU) mechanism and the UA must establish at least one secure session with the host to receive the header intact. Once the header has been received, the UA will cache and apply the policy as per the directives included in the header. </p>
  449. <p> </p>
  450. <h2 id="whydoweneedhpkp">Why Do We Need HPKP?</h2>
  451. <p>Any one of the Certificate Authorities (CAs) in your trust store can issue a certificate for your hostname and the browser will trust it implicitly. This is great in the sense that you can obtain a certificate from any CA of your choosing, but not so great when one gets compromised. </p>
  452. <p> </p>
  453. <h4 id="diginotar">DigiNotar</h4>
  454. <p>Back in 2011 a Dutch CA called DigiNotar was hacked. After gaining access to their systems, the hackers managed to make their way to the CA servers and issued over 500 rogue certificates to themselves. Included in those certificates was one for the google.com domain. This certificate was used to launch a Man in The Middle (MiTM) attack against 300,000 Iranian users of Google services, including GMail. From the end user perspective, the attack was undetectable. The browser received a certifcate for the domain, it was valid and the chain of trust was intact. They had all the indicators of a secure connection but it was totally compromised. There was a lot of speculation about who was responsible for the hack, but from the host's perspective, their users were compromised without any way for them to have known. HPKP changes that. </p>
  455. <p> </p>
  456. <h4 id="startcom">StartCom</h4>
  457. <p>In 2008 and again in 2011 the StartCom CA was breached. The 2008 breach, carried out by a security researcher, resulted in rogue certificates being issued for the paypal.com and verisign.com domains. The 2011 breach, carried out by an unkown attacker, came very close to the crown jewel, the StartCom root key. With access to the StartCom root key, the attacker could have generated a certificate for any domain they like, but not only that, it would have led to the necessary revocation and reissue of every single certificate issued by StartCom. No small task!</p>
  458. <p> </p>
  459. <h2 id="whatdoeshpkpdo">What Does HPKP Do?</h2>
  460. <p>HPKP can be enforced with a very simple HTTP response header. By specifying the fingerprint of certain cryptographic identities, you can force the UA to only accept those identities going forwards. The most ideal solution is to include the fingerprint of your current TLS certificate and at least one backup. The backup can be the fingerprint of a Certificate Signing Request so that you don't have to purchase a backup certificate. If the private key of your certificate were ever compromised, you could use the CSR to request the signing of a new public key. For this to work, the CSR has to be created with a brand new RSA key pair and stored securely offline. As the fingerprint of the CSR was already in the HPKP header, you can switch out to the new certificate without a problem. Using this method, if any CA was ever compromised, even your own CA, any rogue certificates that were issued for your domain would not be accepted by a browser that had received the HPKP header. Because the fingerprint of the rogue certificate has not been received and cached by the browser, it will be rejected and a connection to the site won't be allowed. </p>
  461. <p> </p>
  462. <h2 id="howdowesetuphpkp">How Do We Setup HPKP?</h2>
  463. <p>Setting up and deploying HPKP is incredibly easy, you just have to make sure that you have appropriate backups in place. If you lose all of the backups then you only have until your current certificate expires to get a new policy out to all of your visitors!</p>
  464. <p> </p>
  465. <h4 id="addyourexistingcertificate">Add Your Existing Certificate</h4>
  466. <p>The first step to creating a HPKP policy is to get the fingerprint of your current certificate. </p>
  467. <p><code>openssl x509 -pubkey &lt; tls.crt | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64</code></p>
  468. <p> </p>
  469. <p>That looks like a fairly complicated command but it breaks down to be quite simple.</p>
  470. <p><code>openssl x509</code> We're using the OpenSSL x509 certificate utility which can perform a variety of tasks. All we will be using it for is to output some information about our certificate.<br/>
  471. <code>-pubkey</code> Output the Subject Public Key Info (SPKI) block in PEM format.<br/>
  472. <code>&lt; tls.crt</code> The TLS certificate you want to output the information of.</p>
  473. <p> </p>
  474. <p>This information is then piped into a new command with the <code>|</code> operator.</p>
  475. <p><code>openssl pkey</code> The OpenSSL pkey command allows keys to be converted between forms.<br/>
  476. <code>-pubin</code> Flag that we are providing a public key, as a private key is the default.<br/>
  477. <code>-outform der</code> Set the output format to DER.</p>
  478. <p> </p>
  479. <p>This information is then piped again into the penultimate command.</p>
  480. <p><code>openssl dgst</code> The OpenSSL dgst command is used to output the digest of the provided file.<br/>
  481. <code>-sha256</code> Use the SHA256 hash on the input.<br/>
  482. <code>-binary</code> Output the signature in binary format.</p>
  483. <p> </p>
  484. <p>Lastly, we want to pipe the signature into the <code>base64</code> command to get the fingerprint.<br/></p>
  485. <p>You will end up with a Base64 string that looks something like this:<br/></p>
  486. <p><code>X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg=</code></p>
  487. <p> </p>
  488. <p>Make a note of your fingerprint as you will need it to construct the HPKP header later on.</p>
  489. <p> </p>
  490. <h4 id="creatingabackupcsr">Creating A Backup CSR</h4>
  491. <p>Because we're going to be telling the browser to only accept the specific identities we provide in the header, it's a good idea to have some backups. In fact, I'd say it was absolutely essential. Fortunately, you don't need to have backup certificates. Instead, you create some backup CSRs and include their fingerprints in the header. The only time you will need these is if your private key is compromsied and you need a new certificate, or at your next renewal. It's always a good idea to generate a new key when you renew but you <em>must</em> create the CSRs based on a new key. If your private key is compromised and the CSR was based on your current key pair, it's useless. I'm going to be creating 2 new keys and 2 CSRs so I have a robust backup plan. To start with, generate a new private key. </p>
  492. <p><code>openssl genrsa -out scotthelme.first.key 4096</code></p>
  493. <p> </p>
  494. <p>This command uses the <code>openssl genrsa</code> command to create a new RSA private key. The <code>-out scotthelme.first.key</code> specifies where we would like to save the key and <code>4096</code> sets how large the key should be in bits. Now we have a new private key, we need to generate a CSR for it. </p>
  495. <p><code>openssl req -new -key scotthelme.first.key -sha256 -out scotthelme.first.csr</code></p>
  496. <p> </p>
  497. <p>Using the <code>openssl req</code> (request) command we want to create a <code>-new</code> CSR using the <code>-key scotthelme.first.key</code>. The certificate needs to use the <code>-sha256</code> message digest and finally we specify where to save the CSR <code>-out scotthelme.first.csr</code>. There is then some information you need to provide for the CSR.</p>
  498. <pre><code>Country Name (2 letter code) [AU]:UK
  499. State or Province Name (full name) [Some-State]:Lancashire
  500. Locality Name (eg, city) []:Clitheroe
  501. Organization Name (eg, company) [Internet Widgits Pty Ltd]:Scott Helme
  502. Organizational Unit Name (eg, section) []:IT
  503. Common Name (e.g. server FQDN or YOUR name) []:scotthelme.co.uk
  504. Email Address []:scotthelme@hotmail.com
  505. Please enter the following 'extra' attributes
  506. to be sent with your certificate request
  507. A challenge password []:
  508. An optional company name []:
  509. </code></pre>
  510. <p> </p>
  511. <p>Change the information in the above example to suit your own and note that the last 2 fields can be left empty. Now that the CSR is generated, all we need is the fingerprint to include in the HPKP header and we're good to go. </p>
  512. <p><code>openssl req -pubkey &lt; scotthelme.first.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64</code></p>
  513. <p> </p>
  514. <p>Similar to the previous step on obtaining the fingerprint for the certificate, there are just a couple of differences. </p>
  515. <p><code>openssl req</code> Using the OpenSSL request command again.<br/>
  516. <code>-pubkey &lt; scotthelme.first.csr</code> We want the public key from the CSR we just created.</p>
  517. <p> </p>
  518. <p>That is then piped into another command to convert the key. </p>
  519. <p><code>openssl pkey</code> Using OpenSSL pkey again to convert between formats.<br/>
  520. <code>-pubin</code> Flag that we are providing a public key, as a private key is the default.<br/>
  521. <code>-outform der</code> Set the oputput format to DER. </p>
  522. <p> </p>
  523. <p>The penultimate command to get the SHA256 digest. </p>
  524. <p><code>openssl dgst</code> The OpenSSL dgst command to hash the provided input.<br/>
  525. <code>-sha256</code> Hashed using SHA256.<br/>
  526. <code>-binary</code> Output in binary. </p>
  527. <p> </p>
  528. <p>Finally we pipe that into the <code>base64</code> command to get the fingerprint. </p>
  529. <p><code>MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=</code></p>
  530. <p> </p>
  531. <p>Now, you may have noticed that I used a naming convention in the files through that example that included <code>first</code>, because you guessed it, it's time to rinse and repeat for a second backup! Once you've run all the commands again with <code>second</code>, you should have another private key, CSR and fingerprint. </p>
  532. <p><code>isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg=</code></p>
  533. <p> </p>
  534. <h4 id="configuringnginx">Configuring NginX</h4>
  535. <p>The easiest part of configuring HPKP is adding the header to NginX. Open up the config file for your site and in the server block, add the following with substitutions for your own fingerprints.</p>
  536. <pre><code>add_header Public-Key-Pins 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \
  537. pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \
  538. pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \
  539. max-age=10';
  540. </code></pre>
  541. <p> </p>
  542. <p>This adds a new HTTP response header in NginX, defines the 3 fingerprints that we have created above and finally sets a maximum age for the policy. Save the changes to your config and reload the NginX configuration.</p>
  543. <p><code>sudo service nginx reload</code></p>
  544. <p> </p>
  545. <p>I've used a very short <code>max-age</code> value of 10 seconds for testing purposes so that if something does go wrong, you can remove the header and the policy will expire very quickly, allowing you access to your site again. Once you're happy with the setup and that everything is working you can increase the <code>max-age</code> value to something more suitable like 6-12 months. But remember, the value is in seconds!</p>
  546. <p> </p>
  547. <h4 id="thereportonlyheader">The Report Only Header</h4>
  548. <p>(<strong>added 27th Jan</strong>)</p>
  549. <p>Thanks to Jerome (<a href="https://twitter.com/exploresecurity/status/560020530800377857">@exploresecurity</a>) and his follow-up <a href="http://www.exploresecurity.com/five-considerations-for-http-public-key-pinning-hpkp/">article</a>, I'm adding in this section on a better way to test your HPKP header. Using the HPKP Report Only header (<code>Public-Key-Pins-Report-Only</code>), you can issue your HPKP policy and test the impact without the risk of a failed connection if you get it wrong. In the same way as the report only header for CSP works, the browser will receive the header and output any information about violations to the console and to the <code>report-uri</code>, if one is provided, but it will not block the connection. I have covered the <code>report-uri</code> directive further on in this blog if you want to implement it, otherwise your header would look something like this.</p>
  550. <pre><code>add_header Public-Key-Pins-Report-Only
  551. 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \
  552. pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \
  553. pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \
  554. includeSubdomains';
  555. </code></pre>
  556. <p> </p>
  557. <p>There are quite a few things to note with using the report only header, and indeed a few other elements of using HPKP in general, so I'd recommend Jerome's article linked above. Even with the testing that you can do with the report only header, I'd still recommend a short <code>max-age</code> value when deploying a policy into a live environment just as an added precaution. (<code>max-age</code> is missing from the example above as report only policies are not cached so the directive is ignored).</p>
  558. <p> </p>
  559. <h4 id="includingsubdomainsinhpkp">Including Subdomains In HPKP</h4>
  560. <p>There are 2 ways of dealing with subdomains that also utilise TLS on your site. You can have each domain issue its own unique HPKP policy that specifies the fingerprints for identities to be used on that domain, or, you can issue a HPKP policy at the top that will cascade down all subdomains by using the <code>includeSubdomains</code> directive. Each method has advantages and drawbacks.</p>
  561. <pre><code>add_header Public-Key-Pins 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \
  562. pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \
  563. pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \
  564. max-age=10; includeSubdomains';
  565. </code></pre>
  566. <p> </p>
  567. <p>For example, if you were to navigate directly to test.scotthelme.co.uk, but scotthelme.co.uk was issuing the HPKP policy for that domain and all subdomains, you wouldn't receive the policy. The HPKP header from scotthelme.co.uk would contain the fingerprints for all certificates used on the site and all subdomains, so would act like a master policy. This policy would need to be issued across all subdomains to be effective. You'd also have to have a lot of backups in there to cover revocations if you were compromised and renewals when they come around. I'd say 2 backups per domain is a good idea. It'd be a cumbersome policy but a 'one size fits all' to be issued across all subdomains. </p>
  568. <p>Issuing a specific HPKP header per subdomain results in a smaller header, but a little more management. You need to track the fingerprints for each subdomains certificate and backups and ensure that they are presented in the correct header. Once setup in this manner, none of the policies can contain the <code>includeSubdomains</code> directive or there is the potential to break access to subdomains. You would have a much nicer header containing only 3 fingerprints per subdomain and the management isn't so bad once setup.</p>
  569. <p> </p>
  570. <h2 id="reportingpinvalidationfailures">Reporting Pin Validation Failures</h2>
  571. <p>Now that we have HPKP setup, we can rest assured that our visitors won't fall victim to a MiTM attack where a rogue certificate is used. That's fantastic news, but wouldn't it be even better if we could not only prevent the attack, but know about it in real time? Well, you can! HPKP has a <code>report-uri</code> directive where you can specify a URI for the UA to POST a JSON formatted failure report to. If the UA tries to connect to your site and the certificate fails to meet the criteria of the HPKP policy, we want to know about it. The report format is specified in the <a href="https://tools.ietf.org/html/draft-ietf-websec-key-pinning-21#section-3">IETF Draft</a>. </p>
  572. <pre><code>{
  573. "date-time": date-time,
  574. "hostname": hostname,
  575. "port": port,
  576. "effective-expiration-date": expiration-date,
  577. "include-subdomains": include-subdomains,
  578. "noted-hostname": noted-hostname,
  579. "served-certificate-chain": [
  580. pem1, ... pemN
  581. ],
  582. "validated-certificate-chain": [
  583. pem1, ... pemN
  584. ],
  585. "known-pins": [
  586. known-pin1, ... known-pinN
  587. ]
  588. }
  589. </code></pre>
  590. <p> </p>
  591. <p>You need to include the directive in the policy and provide a suitable URI that is capable of receiving and processing such reports. </p>
  592. <pre><code>add_header Public-Key-Pins "pin-sha256='X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg='; \
  593. pin-sha256='MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec='; \
  594. pin-sha256='isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg='; \
  595. max-age=10; report-uri='https://report.scotthelme.co.uk'";
  596. </code></pre>
  597. <p> </p>
  598. <p>Now, it goes without saying that the authenticity of these reports can never be assured. We have no way to prevent forged reports being delivered and reports may even contain malicious content like SQL Injection or XSS attempts. They should be treated with care and investigated with that in mind. There's also another problem. Well, actually, there could be a few. First, if an attacker has enough access to be able to MiTM you with a rogue certificate, they're going to be able to kill access to the URI used for reporting HPKP validation failures. Second, I use HSTS on my main domain and I'm even <a href="https://scotthelme.co.uk/hsts-preloading/">HSTS preloaded</a> into Chrome, Firefox and Safari. As a result, HSTS is enforced on all my subdomains with the <code>includeSubdomains</code> directive in my HSTS policy. That also means that we should really be issuing a HPKP Policy on all subdomains if we're doing it right. Well, if we're reporting HPKP validation failures, how would we communicate with the report URI? </p>
  599. <blockquote>
  600. <p>Hosts may set report-uris that use HTTP or HTTPS. If the scheme in
  601. the report-uri is one that uses TLS (e.g. HTTPS), UAs MUST perform
  602. Pinning Validation when the host in the report-uri is a Known Pinned
  603. Host; similarly, UAs MUST apply HSTS if the host in the report-uri is
  604. a Known HSTS Host.</p>
  605. </blockquote>
  606. <p> </p>
  607. <p>Of course, the attacker may only have a certificate for our main domain and the reports may be sent just fine, or the attacker could just block access to the URI, but it presents an interesting predicament. The draft does go on to say:</p>
  608. <blockquote>
  609. <p>In any case of report failure, the UA MAY attempt to re-send the
  610. report later.</p>
  611. </blockquote>
  612. <p> </p>
  613. <p>Perhaps for now this is really the best option the UA has available. We are preventing the attack from taking place with the HPKP header, it would just be nice to know about it so that steps could be taken to resolve the issue. </p>
  614. <p> </p>
  615. <h2 id="conclusion">Conclusion</h2>
  616. <p>It's easy to make your site inaccessible if you get HPKP wrong, but, the same applies to any configuration change really. That said, it's also incredibly easy to do right and hugely improve your security stance. The 3 fingerprints we have set in the HPKP header are the <em>only</em> certificates that a browser will now accept for your site. As mentioned earlier, if a CA gets popped and issues rogue certificates for your domain, any browser that has received this header will reject the rogue certificates. This also means that you need to be cautious about how you backup the newly created keys and CSRs. If your server is compromised, it's no use having them on there as they will be compromised too. They need to be taken off the server and stored in a safe location for when they are needed. If your private key is compromised or your certificate is up for renewal, you will need to use one of the CSRs to obtain a new certificate. At that point, you switch to the new certificate, remove the fingerprint for the certificate that's on the way out, generate a new key pair and CSR, get the fingerprint and put that into the header so you always have 2 backup options. Anyone who has previously cached the policy will accept the new certificate as it was one of your backups and this is how you move forwards. </p>
  617. <p> </p>
  618. <h2 id="hpkpandgooglechrome">HPKP And Google Chrome</h2>
  619. <p>Whilst writing this blog I came across a bug in how Google Chrome handles the HPKP header. You can find details on the bug <a href="https://scotthel.me/r3j">here</a>. The problem was around the handling of the <code>max-age</code> and <code>includeSubdomains</code> directives between HSTS and HPKP policies. If one policy had one of these values set, it would apply to the other. This resulted in my 10 second <code>max-age</code> value in the HPKP policy being ignored and the 31536000 value from my HSTS policy being applied across both. The same thing also happened for my subdomains. As it was now applying the fingerprints for scotthelme.co.uk to all my subdomains, they became inaccessible and the policy had an incredibly long duration too. For anyone who visited my site in the brief window that I had issued the policy, they would not have been able to access my subdomains until the patch for the issue made it to the stable build (or some time soon if they were running <a href="https://www.google.com/chrome/browser/canary.html">Canary</a>), or I included the fingerprints for all of my subdomain certificates and their backups in the policy issued on the scotthelme.co.uk domain at the top. Not an ideal solution but HPKP policy is now being applied properly and here is my final policy.</p>
  620. <pre><code>add_header Public-Key-Pins "pin-sha256='X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \
  621. pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \
  622. pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \
  623. pin-sha256="aVNttuSMXJLW/fZOCaR/27b8k6ll5VDuoaVZf2rHIFU="; \
  624. pin-sha256="I/bAACUzdYEFNw2ZKRaypOyYvvOtqBzg21g9a5WVClg="; \
  625. pin-sha256="Y4/Gxyck5JLLnC/zWHtSHfNljuMbOJi6dRQuRJTgYdo="; \
  626. pin-sha256="9Eef0hLnaqy+EhIy8HnUCqFOKzTIWlNNZ5iCJlqT01E="; \
  627. pin-sha256="t3EPvqF+7XoKypCPHyN1b5uey7zTfIGDHn4oBWz2pds="; \
  628. pin-sha256="zqbcEslrpiH0bA9uhNyl2ovpLEfGJQM/QvZSVumMFJ8="; \
  629. includeSubdomains; max-age=31536000' always;
  630. </code></pre>
  631. <p> </p>
  632. <p><strong>Read More:</strong> <a href="https://scotthelme.co.uk/content-security-policy-an-introduction/">Content Security Policy - An Introduction</a> <strong>-</strong> <a href="https://scotthelme.co.uk/hsts-the-missing-link-in-tls/">HSTS - The missing link in Transport Layer Security</a></p>
  633. <p> </p>
  634. <p>Scott. <br/>
  635. Short URL: <a href="https://scotthel.me/hpkp">https://scotthel.me/hpkp</a></p>
  636. </article>
  637. </section>
  638. <nav id="jumpto">
  639. <p>
  640. <a href="/david/blog/">Accueil du blog</a> |
  641. <a href="https://scotthelme.co.uk/hpkp-http-public-key-pinning/">Source originale</a> |
  642. <a href="/david/stream/2019/">Accueil du flux</a>
  643. </p>
  644. </nav>
  645. <footer>
  646. <div>
  647. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  648. <p>
  649. Bonjour/Hi!
  650. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  651. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  652. </p>
  653. <p>
  654. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  655. </p>
  656. <p>
  657. Voici quelques articles choisis :
  658. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  659. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  660. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  661. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  662. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  663. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  664. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  665. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  666. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  667. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  668. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  669. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  670. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  671. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  672. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  673. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  674. </p>
  675. <p>
  676. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  677. </p>
  678. <p>
  679. Je ne traque pas ta navigation mais mon
  680. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  681. conserve des logs d’accès.
  682. </p>
  683. </div>
  684. </footer>
  685. <script type="text/javascript">
  686. ;(_ => {
  687. const jumper = document.getElementById('jumper')
  688. jumper.addEventListener('click', e => {
  689. e.preventDefault()
  690. const anchor = e.target.getAttribute('href')
  691. const targetEl = document.getElementById(anchor.substring(1))
  692. targetEl.scrollIntoView({behavior: 'smooth'})
  693. })
  694. })()
  695. </script>