2859 lines
268 KiB
HTML
2859 lines
268 KiB
HTML
|
<!DOCTYPE html><html lang="fr">
|
|||
|
<head><meta charset="utf-8">
|
|||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"><title>OpenPGP card, une application de chiffrement pour carte à puce - YannStatic</title>
|
|||
|
|
|||
|
<meta name="description" content="Application chiffrement « OpenPGP » pour cartes à puce au format ISO 7816. Une carte à puce dotée d’une telle application vous permet d’y stocker et de proté...">
|
|||
|
<link rel="canonical" href="https://static.rnmkcy.eu/2019/12/25/openpgp-card-une-application-cryptographique-pour-carte-a-puce.html"><link rel="alternate" type="application/rss+xml" title="YannStatic" href="/feed.xml">
|
|||
|
|
|||
|
<!-- - include head/favicon.html - -->
|
|||
|
<link rel="shortcut icon" type="image/png" href="/assets/favicon/favicon.png"><link rel="stylesheet" href="/assets/css/main.css"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" ><!-- start custom head snippets --><link rel="stylesheet" href="/assets/css/expand.css">
|
|||
|
<!-- end custom head snippets --><script>(function() {
|
|||
|
window.isArray = function(val) {
|
|||
|
return Object.prototype.toString.call(val) === '[object Array]';
|
|||
|
};
|
|||
|
window.isString = function(val) {
|
|||
|
return typeof val === 'string';
|
|||
|
};
|
|||
|
|
|||
|
window.hasEvent = function(event) {
|
|||
|
return 'on'.concat(event) in window.document;
|
|||
|
};
|
|||
|
|
|||
|
window.isOverallScroller = function(node) {
|
|||
|
return node === document.documentElement || node === document.body || node === window;
|
|||
|
};
|
|||
|
|
|||
|
window.isFormElement = function(node) {
|
|||
|
var tagName = node.tagName;
|
|||
|
return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
|
|||
|
};
|
|||
|
|
|||
|
window.pageLoad = (function () {
|
|||
|
var loaded = false, cbs = [];
|
|||
|
window.addEventListener('load', function () {
|
|||
|
var i;
|
|||
|
loaded = true;
|
|||
|
if (cbs.length > 0) {
|
|||
|
for (i = 0; i < cbs.length; i++) {
|
|||
|
cbs[i]();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
return {
|
|||
|
then: function(cb) {
|
|||
|
cb && (loaded ? cb() : (cbs.push(cb)));
|
|||
|
}
|
|||
|
};
|
|||
|
})();
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
window.throttle = function(func, wait) {
|
|||
|
var args, result, thisArg, timeoutId, lastCalled = 0;
|
|||
|
|
|||
|
function trailingCall() {
|
|||
|
lastCalled = new Date;
|
|||
|
timeoutId = null;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
}
|
|||
|
return function() {
|
|||
|
var now = new Date,
|
|||
|
remaining = wait - (now - lastCalled);
|
|||
|
|
|||
|
args = arguments;
|
|||
|
thisArg = this;
|
|||
|
|
|||
|
if (remaining <= 0) {
|
|||
|
clearTimeout(timeoutId);
|
|||
|
timeoutId = null;
|
|||
|
lastCalled = now;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
} else if (!timeoutId) {
|
|||
|
timeoutId = setTimeout(trailingCall, remaining);
|
|||
|
}
|
|||
|
return result;
|
|||
|
};
|
|||
|
};
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var Set = (function() {
|
|||
|
var add = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (data[i] === item) {
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
this.size ++;
|
|||
|
data.push(item);
|
|||
|
return data;
|
|||
|
};
|
|||
|
|
|||
|
var Set = function(data) {
|
|||
|
this.size = 0;
|
|||
|
this._data = [];
|
|||
|
var i;
|
|||
|
if (data.length > 0) {
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
add.call(this, data[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
Set.prototype.add = add;
|
|||
|
Set.prototype.get = function(index) { return this._data[index]; };
|
|||
|
Set.prototype.has = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (this.get(i) === item) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
};
|
|||
|
Set.prototype.is = function(map) {
|
|||
|
if (map._data.length !== this._data.length) { return false; }
|
|||
|
var i, j, flag, tData = this._data, mData = map._data;
|
|||
|
for (i = 0; i < tData.length; i++) {
|
|||
|
for (flag = false, j = 0; j < mData.length; j++) {
|
|||
|
if (tData[i] === mData[j]) {
|
|||
|
flag = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (!flag) { return false; }
|
|||
|
}
|
|||
|
return true;
|
|||
|
};
|
|||
|
Set.prototype.values = function() {
|
|||
|
return this._data;
|
|||
|
};
|
|||
|
return Set;
|
|||
|
})();
|
|||
|
|
|||
|
window.Lazyload = (function(doc) {
|
|||
|
var queue = {js: [], css: []}, sources = {js: {}, css: {}}, context = this;
|
|||
|
var createNode = function(name, attrs) {
|
|||
|
var node = doc.createElement(name), attr;
|
|||
|
for (attr in attrs) {
|
|||
|
if (attrs.hasOwnProperty(attr)) {
|
|||
|
node.setAttribute(attr, attrs[attr]);
|
|||
|
}
|
|||
|
}
|
|||
|
return node;
|
|||
|
};
|
|||
|
var end = function(type, url) {
|
|||
|
var s, q, qi, cbs, i, j, cur, val, flag;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
s[url] = true;
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (cur.urls.has(url)) {
|
|||
|
qi = cur, val = qi.urls.values();
|
|||
|
qi && (cbs = qi.callbacks);
|
|||
|
for (flag = true, j = 0; j < val.length; j++) {
|
|||
|
cur = val[j];
|
|||
|
if (!s[cur]) {
|
|||
|
flag = false;
|
|||
|
}
|
|||
|
}
|
|||
|
if (flag && cbs && cbs.length > 0) {
|
|||
|
for (j = 0; j < cbs.length; j++) {
|
|||
|
cbs[j].call(context);
|
|||
|
}
|
|||
|
qi.load = true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
var load = function(type, urls, callback) {
|
|||
|
var s, q, qi, node, i, cur,
|
|||
|
_urls = typeof urls === 'string' ? new Set([urls]) : new Set(urls), val, url;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (_urls.is(cur.urls)) {
|
|||
|
qi = cur;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
val = _urls.values();
|
|||
|
if (qi) {
|
|||
|
callback && (qi.load || qi.callbacks.push(callback));
|
|||
|
callback && (qi.load && callback());
|
|||
|
} else {
|
|||
|
q.push({
|
|||
|
urls: _urls,
|
|||
|
callbacks: callback ? [callback] : [],
|
|||
|
load: false
|
|||
|
});
|
|||
|
for (i = 0; i < val.length; i++) {
|
|||
|
node = null, url = val[i];
|
|||
|
if (s[url] === undefined) {
|
|||
|
(type === 'js' ) && (node = createNode('script', { src: url }));
|
|||
|
(type === 'css') && (node = createNode('link', { rel: 'stylesheet', href: url }));
|
|||
|
if (node) {
|
|||
|
node.onload = (function(type, url) {
|
|||
|
return function() {
|
|||
|
end(type, url);
|
|||
|
};
|
|||
|
})(type, url);
|
|||
|
(doc.head || doc.body).appendChild(node);
|
|||
|
s[url] = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
return {
|
|||
|
js: function(url, callback) {
|
|||
|
load('js', url, callback);
|
|||
|
},
|
|||
|
css: function(url, callback) {
|
|||
|
load('css', url, callback);
|
|||
|
}
|
|||
|
};
|
|||
|
})(this.document);
|
|||
|
})();
|
|||
|
</script><script>
|
|||
|
(function() {
|
|||
|
var TEXT_VARIABLES = {
|
|||
|
version: '2.2.6',
|
|||
|
sources: {
|
|||
|
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css',
|
|||
|
jquery: '/assets/js/jquery.min.js',
|
|||
|
leancloud_js_sdk: '//cdn.jsdelivr.net/npm/leancloud-storage@3.13.2/dist/av-min.js',
|
|||
|
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js',
|
|||
|
gitalk: {
|
|||
|
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js',
|
|||
|
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
|
|||
|
},
|
|||
|
valine: 'https://unpkg.com/valine/dist/Valine.min.js'
|
|||
|
},
|
|||
|
site: {
|
|||
|
toc: {
|
|||
|
selectors: 'h1,h2,h3'
|
|||
|
}
|
|||
|
},
|
|||
|
paths: {
|
|||
|
search_js: '/assets/search.js'
|
|||
|
}
|
|||
|
};
|
|||
|
window.TEXT_VARIABLES = TEXT_VARIABLES;
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<div class="root" data-is-touch="false">
|
|||
|
<div class="layout--page js-page-root">
|
|||
|
<!----><div class="page__main js-page-main page__viewport hide-footer has-aside has-aside cell cell--auto">
|
|||
|
|
|||
|
<div class="page__main-inner">
|
|||
|
<div class="page__header d-print-none">
|
|||
|
<header class="header"><div class="main">
|
|||
|
<div class="header__title">
|
|||
|
<div class="header__brand">
|
|||
|
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="478.9473684210526" viewbox="0, 0, 400,478.9473684210526"><g id="svgg"><path id="path0" d="M308.400 56.805 C 306.970 56.966,303.280 57.385,300.200 57.738 C 290.906 58.803,278.299 59.676,269.200 59.887 L 260.600 60.085 259.400 61.171 C 258.010 62.428,256.198 63.600,255.645 63.600 C 255.070 63.600,252.887 65.897,252.598 66.806 C 252.460 67.243,252.206 67.600,252.034 67.600 C 251.397 67.600,247.206 71.509,247.202 72.107 C 247.201 72.275,246.390 73.190,245.400 74.138 C 243.961 75.517,243.598 76.137,243.592 77.231 C 243.579 79.293,241.785 83.966,240.470 85.364 C 239.176 86.740,238.522 88.365,237.991 91.521 C 237.631 93.665,236.114 97.200,235.554 97.200 C 234.938 97.200,232.737 102.354,232.450 104.472 C 232.158 106.625,230.879 109.226,229.535 110.400 C 228.933 110.926,228.171 113.162,226.434 119.500 C 226.178 120.435,225.795 121.200,225.584 121.200 C 225.373 121.200,225.200 121.476,225.200 121.813 C 225.200 122.149,224.885 122.541,224.500 122.683 C 223.606 123.013,223.214 123.593,223.204 124.600 C 223.183 126.555,220.763 132.911,219.410 134.562 C 218.443 135.742,217.876 136.956,217.599 138.440 C 217.041 141.424,215.177 146.434,214.532 146.681 C 214.240 146.794,214.000 147.055,214.000 147.261 C 214.000 147.467,213.550 148.086,213.000 148.636 C 212.450 149.186,212.000 149.893,212.000 150.208 C 212.000 151.386,208.441 154.450,207.597 153.998 C 206.319 153.315,204.913 150.379,204.633 147.811 C 204.365 145.357,202.848 142.147,201.759 141.729 C 200.967 141.425,199.200 137.451,199.200 135.974 C 199.200 134.629,198.435 133.224,196.660 131.311 C 195.363 129.913,194.572 128.123,193.870 125.000 C 193.623 123.900,193.236 122.793,193.010 122.540 C 190.863 120.133,190.147 118.880,188.978 115.481 C 188.100 112.928,187.151 111.003,186.254 109.955 C 185.358 108.908,184.518 107.204,183.847 105.073 C 183.280 103.273,182.497 101.329,182.108 100.753 C 181.719 100.177,180.904 98.997,180.298 98.131 C 179.693 97.265,178.939 95.576,178.624 94.378 C 178.041 92.159,177.125 90.326,175.023 87.168 C 174.375 86.196,173.619 84.539,173.342 83.486 C 172.800 81.429,171.529 79.567,170.131 78.785 C 169.654 78.517,168.697 77.511,168.006 76.549 C 167.316 75.587,166.594 74.800,166.402 74.800 C 166.210 74.800,164.869 73.633,163.421 72.206 C 160.103 68.936,161.107 69.109,146.550 69.301 C 133.437 69.474,128.581 70.162,126.618 72.124 C 126.248 72.495,125.462 72.904,124.872 73.033 C 124.282 73.163,123.088 73.536,122.219 73.863 C 121.349 74.191,119.028 74.638,117.061 74.858 C 113.514 75.254,109.970 76.350,108.782 77.419 C 107.652 78.436,100.146 80.400,97.388 80.400 C 95.775 80.400,93.167 81.360,91.200 82.679 C 90.430 83.195,89.113 83.804,88.274 84.031 C 85.875 84.681,78.799 90.910,74.400 96.243 L 73.400 97.456 73.455 106.028 C 73.526 117.055,74.527 121.238,77.820 124.263 C 78.919 125.273,80.400 127.902,80.400 128.842 C 80.400 129.202,81.075 130.256,81.900 131.186 C 83.563 133.059,85.497 136.346,86.039 138.216 C 86.233 138.886,87.203 140.207,88.196 141.153 C 89.188 142.098,90.000 143.104,90.000 143.388 C 90.000 144.337,92.129 148.594,92.869 149.123 C 93.271 149.410,93.600 149.831,93.600 150.059 C 93.600 150.286,93.932 150.771,94.337 151.136 C 94.743 151.501,95.598 153.004,96.237 154.475 C 96.877 155.947,97.760 157.351,98.200 157.596 C 98.640 157.841,99.900 159.943,101.000 162.267 C 102.207 164.817,103.327 166.644,103.825 166.876 C 104.278 167.087,105.065 168.101,105.573 169.130 C 107.658 173.348,108.097 174.093,110.006 176.647 C 111.103 178.114,112.000 179.725,112.000 180.227 C 112.000 181.048,113.425 183.163,114.678 184.200 C 115.295 184.711,117.396 188.733,117.720 190.022 C 117.855 190.562,118.603 191.633,119.381 192.402 C 120.160 193.171,121.496 195.258,122.351 197.039 C 123.206 198.820,124.167 200.378,124.487 200.501 C 124.807 200.624,125.953 202.496,127.034 204.662 C 128.114 206.828,129.676 209.299,130.505 210.153 C 131.333 211.007,132.124 212.177,132.262 212.753 C 132.618 214.239,134.291 217.048,136.288 219.516 C 137.230 220.679,138.000 221.92
|
|||
|
" href="/">YannStatic</a>
|
|||
|
</div>
|
|||
|
<!--<button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button>--><!-- <li><button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button></li> -->
|
|||
|
<!-- Champ de recherche -->
|
|||
|
<div id="searchbox" class="search search--dark" style="visibility: visible">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header"></div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text">
|
|||
|
<!-- <div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div> -->
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
json: '/search.json',
|
|||
|
//searchResultTemplate: '<li><a href="https://static.rnmkcy.eu{url}">{date} {title}</a></li>'
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a></li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
<!-- Fin déclaration champ de recherche -->
|
|||
|
</div>
|
|||
|
<nav class="navigation">
|
|||
|
<ul>
|
|||
|
<li class="navigation__item"><a href="/archive.html">Etiquettes</a></li>
|
|||
|
<li class="navigation__item"><a href="/htmldoc.html">Documents</a></li>
|
|||
|
<li class="navigation__item"><a href="/liens_ttrss.html">Liens</a></li>
|
|||
|
<li class="navigation__item"><a href="/aide-jekyll-text-theme.html">Aide</a></li>
|
|||
|
</ul>
|
|||
|
</nav>
|
|||
|
</div>
|
|||
|
</header>
|
|||
|
|
|||
|
</div>
|
|||
|
<div class="page__content"><div class="main"><div class="grid grid--reverse">
|
|||
|
<div class="col-main cell cell--auto">
|
|||
|
<!-- start custom main top snippet --><div id="results-container" class="search-result js-search-result"></div>
|
|||
|
<!-- end custom main top snippet -->
|
|||
|
<article itemscope itemtype="http://schema.org/Article"><div class="article__header"><header><h1 style="color:Tomato;">OpenPGP card, une application de chiffrement pour carte à puce</h1></header></div>
|
|||
|
<meta itemprop="headline" content="OpenPGP card, une application de chiffrement pour carte à puce">
|
|||
|
<div class="article__info clearfix">
|
|||
|
<ul class="left-col menu"><li>
|
|||
|
<a class="button button--secondary button--pill button--sm" href="/archive.html?tag=chiffrement">chiffrement</a>
|
|||
|
</li></ul>
|
|||
|
<ul class="right-col menu"><li>
|
|||
|
<i class="far fa-calendar-alt"></i> <span title="Création" style="color:#FF00FF">25 déc. 2019</span>
|
|||
|
|
|||
|
<span title="Modification" style="color:#00FF7F"> 9 déc. 2023</span>
|
|||
|
</li></ul>
|
|||
|
</div>
|
|||
|
<meta itemprop="datePublished" content="2023-12-09T00:00:00+01:00">
|
|||
|
<meta itemprop="keywords" content="chiffrement">
|
|||
|
<div class="js-article-content">
|
|||
|
<div class="layout--article">
|
|||
|
<!-- start custom article top snippet -->
|
|||
|
<style>
|
|||
|
#myBtn {
|
|||
|
display: none;
|
|||
|
position: fixed;
|
|||
|
bottom: 10px;
|
|||
|
right: 10px;
|
|||
|
z-index: 99;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: bold;
|
|||
|
border: none;
|
|||
|
outline: none;
|
|||
|
background-color: white;
|
|||
|
color: black;
|
|||
|
cursor: pointer;
|
|||
|
padding: 5px;
|
|||
|
border-radius: 4px;
|
|||
|
}
|
|||
|
|
|||
|
#myBtn:hover {
|
|||
|
background-color: #555;
|
|||
|
}
|
|||
|
</style>
|
|||
|
|
|||
|
<button onclick="topFunction()" id="myBtn" title="Haut de page">⇧</button>
|
|||
|
|
|||
|
<script>
|
|||
|
//Get the button
|
|||
|
var mybutton = document.getElementById("myBtn");
|
|||
|
|
|||
|
// When the user scrolls down 20px from the top of the document, show the button
|
|||
|
window.onscroll = function() {scrollFunction()};
|
|||
|
|
|||
|
function scrollFunction() {
|
|||
|
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
|
|||
|
mybutton.style.display = "block";
|
|||
|
} else {
|
|||
|
mybutton.style.display = "none";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// When the user clicks on the button, scroll to the top of the document
|
|||
|
function topFunction() {
|
|||
|
document.body.scrollTop = 0;
|
|||
|
document.documentElement.scrollTop = 0;
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
|
|||
|
<!-- end custom article top snippet -->
|
|||
|
<div class="article__content" itemprop="articleBody">
|
|||
|
<details>
|
|||
|
<summary><b>Afficher/cacher Sommaire</b></summary>
|
|||
|
<!-- affichage sommaire -->
|
|||
|
<div class="toc-aside js-toc-root"></div>
|
|||
|
</details><p><em>Application chiffrement « OpenPGP » pour cartes à puce au format <a href="http://en.wikipedia.org/wiki/ISO/IEC_7816">ISO 7816</a>. Une carte à puce dotée d’une telle application vous permet d’y stocker et de protéger vos clefs OpenPGP privées.</em></p>
|
|||
|
|
|||
|
<h2 id="présentation">Présentation</h2>
|
|||
|
|
|||
|
<p>La carte OpenPGP a été imaginée par les développeurs à l’origine de <a href="http://www.gnupg.org/">GnuPG</a> et est spécialement conçue pour fonctionner avec ce dernier. Son utilisation est aussi transparente que possible et avoir vos clefs privées sur une carte OpenPGP ne change fondamentalement rien à la manière d’utiliser GnuPG : la seule différence est qu’à chaque fois que GnuPG aura besoin d’accéder à une de vos clefs privées, au lieu de vous demander la phrase de passe protégeant la clef sur votre disque dur, il vous demandera le PIN de la carte.</p>
|
|||
|
|
|||
|
<p>Outre la signature et le déchiffrement de messages OpenPGP, les autres usages possibles de la carte incluent :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>la signature ou le déchiffrement de messages S/MIME ;</li>
|
|||
|
<li>l’authentification auprès d’un serveur SSH ;</li>
|
|||
|
<li>l’authentification auprès d’un serveur TLS ;</li>
|
|||
|
<li>et probablement d’autres qui restent à imaginer ou à mettre en œuvre.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h3 id="les-clefs-cryptographiques">Les clefs cryptographiques</h3>
|
|||
|
|
|||
|
<p>La carte OpenPGP permet de stocker jusqu’à trois clefs privées, une pour chaque type d’utilisation : une clef de (dé)chiffrement, une clef de signature et une clef d’authentification.</p>
|
|||
|
|
|||
|
<p>Le rôle majeur de la carte est de <em>protéger ces clefs privées</em>. Lorsqu’elles sont stockées sur la carte, il est par conception impossible de les en extraire, elles ne peuvent qu’être utilisées « sur place ».</p>
|
|||
|
|
|||
|
<p>Ainsi, toutes les opérations cryptographiques nécessitant l’une de ces clefs sont réalisées directement <em>sur la carte elle-même</em> : l’ordinateur envoie à la carte les données à déchiffrer <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> , signer ou authentifier et il reçoit le résultat de l’opération en retour, sans jamais avoir entraperçu les clefs.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Faire réaliser les opérations cryptographiques par la carte ne les rend pas plus rapides, pour au moins trois raisons :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>
|
|||
|
<p>il est peu probable que le processeur de la carte, même s’il est spécialisé, soit plus véloce que votre CPU ;</p>
|
|||
|
</li>
|
|||
|
<li>
|
|||
|
<p>l’échange de données entre l’ordinateur et la carte introduit un délai non négligeable ;</p>
|
|||
|
</li>
|
|||
|
<li>
|
|||
|
<p>la carte ne réalise en réalité qu’une partie du travail, le reste restant à la charge de l’ordinateur (précisément, entre autres, pour limiter le volume de données à transférer entre les deux appareils) ; par exemple, pour déchiffrer un message OpenPGP, la carte n’est responsable que du déchiffrement de la clef de session symétrique (soit seulement 16 octets pour une clef AES 128 bits) — le déchiffrement du corps du message est fait par l’ordinateur, une fois qu’il a obtenu de la carte la clef de session déchiffrée.</p>
|
|||
|
</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Bref, il ne faut pas compter sur la carte pour <em>accélérer</em> les opérations cryptographiques, ce n’est pas son but.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Pour l’instant, les clefs de la carte ne peuvent être que des clefs RSA — d’autres types de clefs, notamment à base de <a href="http://tools.ietf.org/html/rfc6637">courbes elliptiques</a> dont la prise en charge arrive dans GnuPG 2.1, seront probablement possibles à l’avenir. La spécification garantit une taille minimale pour chaque clef de 1024 bits et n’impose pas de taille maximale. En pratique, toutes les implémentations courantes devraient permettre des clefs de 2048 bits et certaines permettent des clefs de 4096 bits (c’est notamment le cas de l’implémentation de référence de ZeitControl — voir plus loin).</p>
|
|||
|
|
|||
|
<h3 id="sécurité-de-la-carte">Sécurité de la carte</h3>
|
|||
|
|
|||
|
<p>La carte OpenPGP est protégée par deux ou (optionnellement) trois codes PIN, désignés PW1 ou « PIN utilisateur » (<em>User PIN</em>, parfois appelé seulement « PIN », par opposition au suivant), PW3 ou « PIN administrateur » (<em>Admin PIN</em>) et RC (<em>Resetting Code</em>, optionnel).</p>
|
|||
|
|
|||
|
<p>Les opérations de la carte peuvent être classées en fonction du PIN nécessaire pour les réaliser :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>aucun PIN n’est nécessaire pour <em>lire</em> les données de la carte ;</li>
|
|||
|
<li>le PIN utilisateur est demandé pour toute opération exploitant les clefs privées (signature, déchiffrement, authentification) et pour changer le PIN utilisateur lui-même ;</li>
|
|||
|
<li>le PIN administrateur est demandé pour presque toutes les <em>écritures</em> sur la carte (modification des informations stockées sur la carte, importation ou génération de clefs privées, changement d’un PIN autre que le PIN utilisateur).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>À chaque PIN est associé un compteur d’essai (<em>Retry Counter</em>), qui est décrémenté à chaque saisie incorrecte du PIN. Tant que le compteur est supérieur à zéro, il suffit de saisir le PIN correct pour remettre le compteur à trois. Lorsque le compteur tombe à zéro, la vérification du PIN correspondant n’est plus possible et toutes les opérations dépendantes de cette vérification sont interdites. Dans ce cas, il faut enregistrer un nouveau PIN pour remettre le compteur à trois.</p>
|
|||
|
|
|||
|
<p>Concrètement, cela signifie ① que trois saisies erronées consécutives du PIN utilisateur entraînent un blocage de la carte (puisque toutes les opérations cryptographiques nécessitent de vérifier le PIN utilisateur), ② que la carte peut être débloquée en changeant le PIN utilisateur, après avoir saisi le PIN administrateur, ③ qu’après trois saisies erronées consécutives du PIN administrateur, la carte est <em>irréversiblement</em> bloquée (puisqu’il faudrait changer le PIN administrateur pour remettre son compteur d’essai à trois, or cela nécessite de vérifier le PIN administrateur, ce qui n’est justement pas possible si son compteur est à zéro).</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Certaines implémentations prévoient néanmoins la possibilité de ré-initialiser complètement une carte dont les compteurs d’essai des PIN utilisateur et administrateur sont tous les deux à zéro. La ré-initialisation efface toutes les données de la carte, restaure les PIN utilisateur et administrateur par défaut et remet leur compteur à trois.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Et le <em>Resetting Code</em> ? S’il est défini, il peut être utilisé à la place du PIN administrateur pour ré-initialiser le PIN utilisateur et donc débloquer la carte. Peu pertinent pour une utilisation individuelle, le RC est surtout utile dans les situations où la carte est délivrée par une autorité à un utilisateur qui n’est pas supposé en modifier le contenu et à qui on ne veut donc pas révéler le PIN administrateur, mais que l’on veut tout de même autoriser à débloquer sa carte tout seul. (Le RC est donc assimilable au code « PUK » utilisé pour débloquer une carte SIM.) Par défaut, aucun <em>Resetting Code</em> n’est défini et son compteur d’essai est à zéro.</p>
|
|||
|
|
|||
|
<p>Le PIN utilisateur doit faire au minimum 6 caractères, le PIN administrateur et le <em>Resetting Code</em> au minimum 8. La taille maximale n’est pas définie dans la spécification (elle est de 32 caractères pour chaque PIN sur l’implémentation de référence de ZeitControl). Il est possible d’utiliser n’importe quel caractère UTF-8 et non pas seulement des chiffres, mais un code non-numérique ne pourra pas être saisi sur le clavier intégré à certains lecteurs de carte.</p>
|
|||
|
|
|||
|
<p>Dans la suite de ce document, « PIN » sans précision désignera implicitement le PIN utilisateur. Lorsqu’il sera question du PIN administrateur, il sera toujours désigné explicitement.</p>
|
|||
|
|
|||
|
<h3 id="données-stockées-sur-la-carte">Données stockées sur la carte</h3>
|
|||
|
|
|||
|
<p>Outre les clefs privées, la carte OpenPGP contient un certain nombre de champs (“<em>Data Objects</em>” ou DO, dans le jargon des cartes à puce) pour stocker des données supplémentaires.</p>
|
|||
|
|
|||
|
<h4 id="les-champs-à-usage-défini">Les champs à usage défini</h4>
|
|||
|
|
|||
|
<p>Ce sont des champs dont l’usage est explicitement défini dans la spécification de la carte OpenPGP. Toutefois, GnuPG n’utilise pas lui-même la plupart de ces champs — il permet de les consulter et les modifier, mais c’est à l’utilisateur ou à d’autres programmes exploitant la carte de leur trouver une utilisation.</p>
|
|||
|
|
|||
|
<p>Ces champs sont :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>le nom du détenteur de la carte ;</li>
|
|||
|
<li>son sexe ;</li>
|
|||
|
<li>ses préférences linguistiques : une liste de 1 à 4 codes de langue au format <a href="http://fr.wikipedia.org/wiki/Liste_des_codes_ISO_639-1">ISO 639-1</a> ;</li>
|
|||
|
<li>son « identifiant » : la spécification mentionne la possibilité qu’un système d’authentification basée sur la carte OpenPGP utilise ce champ pour savoir sur quel compte connecter l’utilisateur une fois qu’il a saisi son PIN, mais je ne connais pas de systèmes qui le font ;</li>
|
|||
|
<li>un certificat : typiquement, un certificat X.509, mais n’importe quel autre format est possible (GnuPG traite ce champ comme un simple blob binaire) ;</li>
|
|||
|
<li>une URL vers la clef publique de l’utilisateur : GnuPG s’en sert pour rapatrier automatiquement la clef publique en question, ce qui permet d’avoir rapidement un trousseau opérationnel lorsque vous êtes amenés à travailler sur une machine qui n’est pas la vôtre ;</li>
|
|||
|
<li>jusqu’à trois « empreintes de clefs de confiance » (“<em>CA Fingerprints</em>”) : des empreintes SHA-1 de clefs considérées valides et auxquelles il est fait une confiance absolue — là encore, GnuPG ne se sert pas de ces champs et je ne connais aucun exemple concret d’utilisation.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h4 id="les-champs-privés-ou-à-usage-indéfini">Les champs privés ou à usage indéfini</h4>
|
|||
|
|
|||
|
<p>En plus des champs ci-dessus, la version 2.0 de la spécification définit 4 champs inutilisés, de 254 octets chacun, dont l’usage est entièrement et officiellement laissé à la discrétion de l’utilisateur : les “<em>Private Use DOs</em>” 1 à 4.</p>
|
|||
|
|
|||
|
<p>La différence entre ces 4 champs réside dans le PIN demandé pour y accéder en lecture ou en écriture :</p>
|
|||
|
|
|||
|
<table>
|
|||
|
<thead>
|
|||
|
<tr>
|
|||
|
<th>private #</th>
|
|||
|
<th>.</th>
|
|||
|
<th>Lecture</th>
|
|||
|
<th>.</th>
|
|||
|
<th>Écriture</th>
|
|||
|
</tr>
|
|||
|
</thead>
|
|||
|
<tbody>
|
|||
|
<tr>
|
|||
|
<td>Private DO 1</td>
|
|||
|
<td>.</td>
|
|||
|
<td>aucun PIN</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN utilisateur</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td>Private DO 2</td>
|
|||
|
<td>.</td>
|
|||
|
<td>aucun PIN</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN administrateur</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td>Private DO 3</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN utilisateur</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN administrateur</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td>Private DO 4</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN administrateur</td>
|
|||
|
<td>.</td>
|
|||
|
<td>PIN administrateur</td>
|
|||
|
</tr>
|
|||
|
</tbody>
|
|||
|
</table>
|
|||
|
|
|||
|
<p>(On notera que ces conditions s’écartent quelque peu du principe général <a href="#s%C3%A9curit%C3%A9-de-la-carte">énoncé plus haut</a> selon lequel la lecture de la carte ne demande pas de PIN et l’écriture demande le PIN administrateur.)</p>
|
|||
|
|
|||
|
<p>Ces champs sont vides par défaut, il appartient à chacun d’imaginer l’usage qu’il peut en faire. (La carte fournie par la FSFE à ses adhérents contient, dans le Private DO 2, le numéro, le nom et l’adresse électronique en @fsfe.org de l’adhérent.)</p>
|
|||
|
|
|||
|
<h2 id="le-matériel">Le matériel</h2>
|
|||
|
|
|||
|
<h3 id="la-carte-à-puce">La carte à puce</h3>
|
|||
|
|
|||
|
<p>La « carte OpenPGP » est juste une spécification ; tout le monde peut l’implémenter sur le support de son choix.</p>
|
|||
|
|
|||
|
<p>Actuellement, l’implémentation la plus répandue est réalisée sur la carte <a href="http://www.zeitcontrol.de/en/products/chip-cards/processor-chip-cards/basiccard">BasicCard</a> de ZeitControl, une carte à puce programmable en BASIC. Elle est distribuée par <a href="http://shop.kernelconcepts.de/product_info.php?cPath=1_26&products_id=42">Kernel Concepts</a>. La <a href="http://www.fsfe.org/">Free Software Foundation Europe</a> fournit également à tous ses nouveaux <em>fellows</em> un <a href="http://wiki.fsfe.org/FellowshipSmartCard">exemplaire personnalisé de cette carte</a>, aux couleurs de la FSFE et sérigraphié au nom de l’adhérent.</p>
|
|||
|
|
|||
|
<p><a href="http://www.fsij.org/doc-gnuk/intro.html">Gnuk</a> est une implémentation de la carte OpenPGP pour microcontrôleur STM32F103, permettant notamment de réaliser un <em>token USB</em> — un périphérique qui apparaît, aux yeux de l’ordinateur, comme un lecteur de cartes à puce classique contenant une carte unique insérée en permanence. Le <a href="http://www.seeedstudio.com/wiki/FST-01">FST-01</a> est un exemple d’un tel token basé sur Gnuk, il est disponible auprès de <a href="http://www.seeedstudio.com/depot/FST-01-with-White-Enclosure-p-1279.html">Seeed Bazaar</a>.</p>
|
|||
|
|
|||
|
<p>Le <a href="https://www.assembla.com/spaces/cryptostick/wiki/Home">CryptoStick</a> est un autre token USB formé d’une carte OpenPGP physique (celle de ZeitControl, <em>a priori</em>) embarquée dans un boîtier USB. Le projet <a href="https://www.crypto-stick.com/2013/project-roadmap">envisage</a> aussi de créer une version basée sur Gnuk.</p>
|
|||
|
|
|||
|
<p>Il existe aussi <a href="https://github.com/FluffyKaon/OpenPGP-Card">plusieurs</a> <a href="http://sourceforge.net/projects/javacardopenpgp/">implémentations</a> ciblant des [[Java Card]]s (cartes à puce programmables en Java). La <a href="http://www.yubico.com/products/yubikey-hardware/yubikey-neo/">YubiKey NEO</a> en contient une, désactivée par défaut — Yubico fournit <a href="http://www.yubico.com/2012/12/yubikey-neo-openpgp/">les instructions pour l’activer</a>.</p>
|
|||
|
|
|||
|
<h3 id="le-lecteur-de-cartes">Le lecteur de cartes</h3>
|
|||
|
|
|||
|
<p>À moins d’opter pour un token USB comme le FST-01, le CryptoStick ou la YubiKey, un lecteur de cartes à puce est évidemment nécessaire pour utiliser une carte OpenPGP.</p>
|
|||
|
|
|||
|
<p>Il existe une grande variété de lecteurs, voici les principaux points à considérer pour choisir le sien.</p>
|
|||
|
|
|||
|
<p><strong>Port série, port PCMCIA ou port USB ?</strong> La plupart des lecteurs aujourd’hui sont des lecteurs USB — et, faute d’expérience de l’auteur avec les autres types de lecteurs, ce sont les seuls dont il sera question dans cet article.</p>
|
|||
|
|
|||
|
<p><strong>Clavier intégré ?</strong> Un petit nombre de lecteurs contiennent un clavier intégré permettant de saisir le PIN directement sur le lecteur (par exemple <a href="http://www.scm-pc-card.de/index.php?page=product&function=show_product&lang=en&product_id=670">le SPR 332 de SCM Microsystems</a>). L’intérêt en termes de sécurité est que dans ce cas, le PIN ne passe jamais par l’ordinateur et n’est pas susceptible d’être enregistré par un éventuel <em>keylogger</em> ou autre logiciel malveillant. Attention, le clavier ne comprend la plupart du temps que des chiffres, interdisant la saisie d’un PIN qui ne serait pas entièrement numérique ; certains de ces lecteurs limitent aussi la longueur maximale du PIN, qui peut être plus courte que ce qu’autorise la carte OpenPGP.</p>
|
|||
|
|
|||
|
<p><strong>Prise en charge de l’<em>Extended APDU</em> ?</strong> Les APDU (<em>Application Protocol Data Unit</em>) sont les messages échangés entre la carte à puce et son lecteur. Ils peuvent être <em>short</em> (les données transmises sont limitées à 256 octets) ou <em>extended</em> (données limitées à 65536 octets). Pour importer une clef de 1024 bits sur la carte OpenPGP, le lecteur doit prendre en charge les <em>extended APDU</em>.</p>
|
|||
|
|
|||
|
<p>Un autre point important est bien sûr la prise en charge du lecteur sous votre système. La question des pilotes sera abordée dans la section suivante, mais disons tout de suite que sous GNU/Linux, vous vous simplifierez probablement la tâche en choisissant un lecteur conforme à la norme CCID (<em>Chip/Card Interface Device</em>). Le <a href="https://pcsclite.alioth.debian.org/ccid.html">pilote ccid</a> prend en charge une grande partie de ces lecteurs et ses développeurs tiennent à jour <a href="https://pcsclite.alioth.debian.org/ccid.html#readers">des listes des lecteurs pris en charge ou non</a> — et indiquent en plus pour chaque lecteur les problèmes potentiels comme l’absence de gestion des <em>extended APDU</em>.</p>
|
|||
|
|
|||
|
<p>L’auteur de ces lignes a choisi pour sa part le <a href="http://identive-infrastructure.com/pdf/usa/Dat_SCR3500_e.pdf">SCR 3500 SmartFold</a> de SCM Microsystems (maintenant Identive), un petit lecteur compact et aisément transportable. Il fait partie des lecteurs qui « devraient fonctionner » (<em>should work readers</em>) avec le pilote ccid et je confirme qu’il fonctionne bien. Pour ce que ça vaut, j’en suis satisfait, même si je le trouve un peu trop fragile pour l’utilisation nomade à laquelle sa taille le destine.</p>
|
|||
|
|
|||
|
<h2 id="les-logiciels">Les logiciels</h2>
|
|||
|
|
|||
|
<p>En règle générale, l’utilisation de cartes à puce nécessite une pile logicielle à trois niveaux : un pilote pour le lecteur de cartes, un <em>middleware</em> exposant une API standard d’accès aux cartes à puce (permettant aux applications de faire abstraction du matériel et des pilotes) et les applications utilisatrices.</p>
|
|||
|
|
|||
|
<h3 id="les-pilotes-de-lecteurs-de-carte">Les pilotes de lecteurs de carte</h3>
|
|||
|
|
|||
|
<p>Le pilote ccid déjà évoqué ci-dessus prend en charge un grand nombre de lecteurs USB compatibles avec la norme CCID. Cela inclut également les <em>tokens</em> USB comme le CryptoStick, les <em>tokens</em> basés sur Gnuk ou le YubiKey NEO, qui sont tous compatibles CCID.</p>
|
|||
|
|
|||
|
<p>Il existe également des pilotes plus spécifiques pour des modèles précis de lecteurs. Pour les utilisateurs de Debian (et dérivés), le paquet virtuel <a href="https://packages.debian.org/wheezy/pcsc-ifd-handler">pcsc-ifd-handler</a> en fournit commodément une liste.</p>
|
|||
|
|
|||
|
<p>Des modèles non-pris en charge par les pilotes libres disposent parfois d’un pilote propriétaire fourni par le constructeur. C’est le cas par exemple des lecteurs de la gamme HID Omnikey, dont les pilotes sont disponibles sur <a href="http://www.hidglobal.com/drivers">www.hidglobal.com/drivers</a>.</p>
|
|||
|
|
|||
|
<p>Libre ou non, le pilote d’un lecteur USB doit être installé dans un dossier appelé <code class="language-plaintext highlighter-rouge">_nom-du-pilote_.bundle</code>, lui-même situé dans le dossier <code class="language-plaintext highlighter-rouge">/usr/local/pcsc/drivers</code> (par défaut — ce dossier est configurable à la compilation de PCSC-Lite, vous pouvez consulter la page de manuel de pcscd(8) pour connaître le chemin effectif sur votre système). Vous pouvez bien sûr ignorer cette étape si le pilote est fourni dans les paquets de votre distribution.</p>
|
|||
|
|
|||
|
<h3 id="le-middleware-pcsc-lite">Le middleware PCSC-Lite</h3>
|
|||
|
|
|||
|
<p><a href="http://pcsclite.alioth.debian.org/pcsclite.html">PCSC-Lite</a> est une implémentation libre pour systèmes Unix-like (y compris Mac OS X) de l’API Windows SCard, introduite dans Windows XP/Windows Server 2003 et normalisée par la suite sous le nom de <a href="http://www.pcscworkgroup.com/specifications/overview.php">spécification PC/SC</a>.</p>
|
|||
|
|
|||
|
<p>PCSC-Lite comprend deux composants : un démon (pcscd) qui gère le lecteur et communique avec la carte et une bibliothèque (libpcsclite.so), qui expose l’API PC/SC aux programmes qui lui sont liés.</p>
|
|||
|
|
|||
|
<p>Le paquet de PCSC-Lite fourni par votre distribution devrait faire le nécessaire pour que le démon pcscd soit automatiquement lancé au démarrage. Si ce n’était pas le cas, consultez la documentation de votre système pour savoir comment ajouter le script shell / le <em>job</em> Upstart / l’unité Systemd / le truc lanceur de machins appropriés à votre système.</p>
|
|||
|
|
|||
|
<p>À son lancement, le démon pcscd détecte automatiquement les lecteurs de carte présents sur le système. En revanche, si vous connectez votre lecteur série ou PCMCIA <em>après</em> le lancement du démon, celui-ci ne le détectera pas automatiquement, il faudra utiliser <code class="language-plaintext highlighter-rouge">pcscd --hotplug</code> pour l’inciter à rescanner ; ceci n’est pas nécessaire pour les lecteurs USB (cf <a href="//linuxfr.org/news/openpgp-card-une-application-cryptographique-pour-carte-a-puce#comment-1579401">commentaire</a>.</p>
|
|||
|
|
|||
|
<p>Sinon il est possible d’utiliser une règle Udev semblable à la suivante (à ajouter dans <code class="language-plaintext highlighter-rouge">/etc/udev/rules.d/90-local.rules</code>, ou l’équivalent pour votre distribution) :</p>
|
|||
|
|
|||
|
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="04e6", ATTR{idProduct}=="5410", RUN+="/usr/sbin/pcscd --hotplug"
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>où <em>04e6</em> et <em>5410</em> identifient le fournisseur et le modèle de votre lecteur, respectivement (les valeurs données ici correspondent au SCR 3500 de l’auteur, vous pouvez trouver celles correspondants à votre modèle avec <code class="language-plaintext highlighter-rouge">lsusb -v</code> par exemple).</p>
|
|||
|
|
|||
|
<p>Si pcscd tourne sous un compte utilisateur dédié non-privilégié (ce qui est recommandé), il faut de plus s’assurer que ledit compte possède les droits sur le lecteur de carte. Le plus simple pour cela est de compléter la règle Udev ci-dessus en ajoutant les clefs suivantes (en supposant ici que pcscd tourne sous le compte utilisateur <em>scard</em>) :</p>
|
|||
|
|
|||
|
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[…], OWNER="scard", MODE="600"
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="gnupg">GnuPG</h3>
|
|||
|
|
|||
|
<p>GnuPG est bien entendu la principale application utilisatrice de la carte OpenPGP et c’est la seule qui sera abordée dans cet article. Sachez néanmoins que d’autres applications peuvent exploiter cette carte. En effet, quoique développée spécifiquement pour les besoins de GnuPG, la carte OpenPGP est compatible avec le <a href="http://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-15-cryptographic-token-information-format.htm">standard PKCS#15</a>, qui définit une manière commune de stocker et d’accéder à des données cryptographiques sur une carte à puce. En fait, on peut voir la carte OpenPGP comme une carte PKCS#15, avec quelques spécificités propres à OpenPGP en plus. (Réciproquement, GnuPG peut aussi exploiter des cartes PKCS#15 standards en plus de « sa » carte OpenPGP.)</p>
|
|||
|
|
|||
|
<h4 id="à-propos-des-versions-de-gnupg">À propos des versions de GnuPG</h4>
|
|||
|
|
|||
|
<p>Il existe actuellement trois variantes de GnuPG : la branche 1.x, dite « classique », dont la dernière version à l’heure où ces lignes sont écrites est la 1.4.18 ; la branche 2.0.x dite « stable » (dernière version : 2.0.26) ; et la nouvelle version 2.1.1 dite « moderne » <a href="http://lists.gnupg.org/pipermail/gnupg-users/2014-December/051953.html">qui vient tout juste de sortir</a> le 16 décembre 2014.</p>
|
|||
|
|
|||
|
<p>Toutes les variantes permettent d’utiliser la carte OpenPGP, mais GnuPG 1.x seul ne permet que les opérations « de base » (éditer le contenu de la carte, déchiffrer et signer des messages OpenPGP). Les usages plus avancés qui seront décrits plus loin et, notamment, l’utilisation de la clef d’authentification, nécessitent les outils auxiliaires apportés par la branche 2.x, en particulier l’agent GnuPG (gpg-agent). GnuPG 2.x est donc conseillé pour exploiter tout le potentiel de la carte OpenPGP.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Incidemment, avec la sortie de GnuPG 2.1.0 il semble clair que GnuPG 1.x n’a probablement pas beaucoup d’avenir sur le bureau. Entre autres indices :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Les algorithmes à base de courbes elliptiques, qui viennent de faire leur entrée dans GnuPG 2.1, <a href="http://lists.gnupg.org/pipermail/gnupg-users/2014-November/051294.html">ne seront pas pris en charge par GnuPG 1.x</a>.</li>
|
|||
|
<li>GnuPG 2.1 introduit <a href="https://www.gnupg.org/faq/whats-new-in-2.1.html#nosecring">un nouveau format de stockage des trousseaux de clefs</a> incompatible avec celui des précédentes versions. Auparavant, il était possible d’utiliser conjointement GnuPG 1.x et GnuPG 2.0.x avec un trousseau commun, puisque les deux versions utilisaient le même format. Désormais, si la cohabitation de GnuPG 1.x et GnuPG 2.1 est toujours possible, les deux versions utiliseront chacune un trousseau distinct et les modifications faites avec GnuPG 1.x ne seront pas répercutées sur le trousseau de GnuPG 2.1 et inversement.</li>
|
|||
|
<li>Werner Koch lui-même <a href="http://lists.gnupg.org/pipermail/gnupg-devel/2014-November/029091.html">explique</a> qu’il maintient GnuPG 1.x principalement pour les besoins des anciennes plates-formes et des serveurs.</li>
|
|||
|
</ul>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Dans le reste de ce document, je supposerai l’utilisation de GnuPG 2.x, en faisant lorsque c’est nécessaire la distinction entre 2.0 et 2.1. Les captures de sortie standard seront issues de GnuPG 2.0.26, mais sauf mention contraire, tous les exemples seront applicables à GnuPG 2.1.</p>
|
|||
|
|
|||
|
<p>Si vous tenez à utiliser GnuPG 1.x, sachez que vous pouvez l’utiliser conjointement avec l’agent GnuPG de la branche 2.x. Dans ce cas, certains des « usages avancés » vous seront accessibles, notamment l’authentification SSH. En revanche, si vous tenez à utiliser GnuPG 1.x <em>seulement</em> (sans agent), vous ne pourrez pas faire usage de la clef d’authentification (en tout cas pas avec GnuPG).</p>
|
|||
|
|
|||
|
<h4 id="comment-gnupg-accède-au-lecteur-de-carte">Comment GnuPG accède au lecteur de carte</h4>
|
|||
|
|
|||
|
<p>Les applications de GnuPG 2.x (<code class="language-plaintext highlighter-rouge">gpg2</code> et <code class="language-plaintext highlighter-rouge">gpgsm</code>) n’accèdent jamais directement au lecteur de carte, dont ils sont séparés par plusieurs niveaux d’indirection. Elles interagissent seulement avec l’agent GnuPG, un démon qui gère les clefs secrètes et les phrases de passe pour l’ensemble du système GnuPG 2.x.</p>
|
|||
|
|
|||
|
<p>L’agent GnuPG lui-même délègue la gestion des cartes à puce à un autre démon, Scdaemon (SmartCard Daemon). C’est lui seul qui s’occupe de détecter le lecteur de carte et de communiquer avec la carte qu’il contient. Il utilise <a href="https://www.gnupg.org/documentation/manuals/gnupg/Scdaemon-Protocol.html#Scdaemon-Protocol">un protocole ad-hoc</a> pour recevoir ses instructions de l’agent GnuPG.</p>
|
|||
|
|
|||
|
<p>Scdaemon dispose de deux options pour accéder au lecteur de carte. Dans le cas général, il utilise le <em>middleware</em> PCSC-Lite que nous avons vu précédemment. Mais il peut aussi utiliser un petit pilote CCID interne, qui lui permet de communiquer directement avec les lecteurs USB compatibles sans passer par un intermédiaire.</p>
|
|||
|
|
|||
|
<p>GnuPG 1.x, de son côté, <em>peut</em> utiliser l’agent GnuPG (ce qu’il fait automatiquement par défaut, s’il détecte qu’un agent est en cours d’exécution), auquel cas tout se passe comme avec GnuPG 2.x. Mais il peut aussi se passer des démons auxiliaires et communiquer seul avec le lecteur de carte.</p>
|
|||
|
|
|||
|
<p><img src="http://www.incenp.org/files/misc/2014/gnupg-smartcard-access.svg" alt="Architecture de GnuPG pour l’accès aux cartes à puce"></p>
|
|||
|
|
|||
|
<h4 id="le-pilote-ccid-interne">Le pilote CCID interne</h4>
|
|||
|
|
|||
|
<p>Comme évoqué à l’instant, GnuPG (toutes versions confondues) contient un petit pilote générique pour les lecteurs compatibles CCID. Ses développeurs décrivent ce pilote comme “un pilote limité […] à utiliser en dernier recours quand rien d’autre ne fonctionne ou que l’on tient à avoir un système minimal pour des raisons de sécurité”.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>
|
|||
|
|
|||
|
<p>Si votre lecteur de carte est pris en charge par le pilote interne, vous pouvez être tenté de simplifier la pile logicielle en vous passant du <em>middleware</em> PCSC-Lite. Gardez toutefois à l’esprit que le pilote interne est spécifique à GnuPG : si vous prévoyez d’utiliser votre lecteur pour d’autres applications (ne serait-ce que pour explorer, par curiosité, le contenu de votre carte bancaire, de votre carte vitale ou de votre carte de transports en commun, par exemple avec <a href="http://pannetrat.com/Cardpeek/">Cardpeek</a>), vous aurez besoin de PCSC-Lite de toute façon.</p>
|
|||
|
|
|||
|
<p>Pour utiliser PCSC-Lite, il suffit que le démon de PCSC-Lite soit démarré : GnuPG tentera d’accéder au lecteur avec son pilote interne, échouera puisque le lecteur sera déjà monopolisé par pcscd et se rabattra, automatiquement, sur PCSC-Lite (vous pouvez éventuellement désactiver explicitement le pilote interne, en ajoutant l’option <code class="language-plaintext highlighter-rouge">disable-ccid</code> dans le fichier de configuration de scdaemon, <code class="language-plaintext highlighter-rouge">$GNUPGHOME/scdaemon.conf</code>).</p>
|
|||
|
|
|||
|
<p>Inversement, pour utiliser le pilote interne, assurez-vous que PCSC-Lite n’est pas installé (ou, <em>a minima</em>, que son démon n’est pas démarré) et que votre propre compte utilisateur, sous l’identité duquel tourne GnuPG, a accès au lecteur de carte.</p>
|
|||
|
|
|||
|
<h4 id="lagent-gnupg">L’agent GnuPG</h4>
|
|||
|
|
|||
|
<p>L’agent GnuPG (<code class="language-plaintext highlighter-rouge">gpg-agent</code>) est indispensable au bon fonctionnement de GnuPG 2.x. et à l’exploitation de toutes les fonctionnalités de la carte OpenPGP. GnuPG 2.1.0 a introduit quelques changements importants dans la façon d’utiliser l’agent, il est donc utile de profiter de cet article pour les aborder.</p>
|
|||
|
|
|||
|
<p><strong>GnuPG 2.0.x</strong></p>
|
|||
|
|
|||
|
<p>L’agent GnuPG 2.0.x peut être soit lancé au démarrage d’une session utilisateur, soit à la demande sitôt qu’un composant de GnuPG en a besoin.</p>
|
|||
|
|
|||
|
<p>Pour démarrer l’agent en même temps que votre session graphique, ajoutez simplement la ligne suivante à votre fichier <code class="language-plaintext highlighter-rouge">~/.xprofile</code> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> eval $(gpg-agent --daemon)
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Puis redémarrez votre session. L’agent sera lancé en même temps et écoutera sur une <em>socket</em> Unix au nom aléatoire et la variable GPG_AGENT_INFO sera définie dans l’environnement, pour permettre aux composants de GnuPG de trouver l’agent en cours d’exécution.</p>
|
|||
|
|
|||
|
<p>Notez que votre gestionnaire de paquets s’est peut-être déjà chargé de ça pour vous — c’est au moins le cas sur Debian, où le paquet <code class="language-plaintext highlighter-rouge">gnupg-agent</code> ajoute un script <code class="language-plaintext highlighter-rouge">/etc/X11/Xsession.d/90gpg-agent</code> à cet effet.</p>
|
|||
|
|
|||
|
<p>Si vous préférez que l’agent soit démarré seulement quand il est nécessaire plutôt que systématiquement au début de la session, il doit être configuré pour utiliser une <em>socket</em> au nom constant et prédéfinie (<code class="language-plaintext highlighter-rouge">$GNUPGHOME/S.gpg-agent</code>). Cela se fait soit à la compilation, en passant l’option <code class="language-plaintext highlighter-rouge">--enable-standard-socket</code> au script <code class="language-plaintext highlighter-rouge">configure</code>, soit à l’exécution en ajoutant l’option <code class="language-plaintext highlighter-rouge">use-standard-socket</code> dans le fichier de configuration de l’agent (<code class="language-plaintext highlighter-rouge">$GNUPGHOME/gpg-agent.conf</code>). Une fois l’agent configuré ainsi, la ligne ci-dessus dans le fichier <code class="language-plaintext highlighter-rouge">~/.xprofile</code> n’est plus nécessaire, le premier programme de GnuPG 2.0.x ayant besoin de l’agent en invoquera automatiquement un s’il ne détecte pas la <em>socket</em> « standard ».</p>
|
|||
|
|
|||
|
<p>Le démarrage à la demande a toutefois l’inconvénient que seuls les composants de GnuPG 2.x sont capables de démarrer l’agent ainsi. GnuPG 1.x par exemple ne peut pas le faire et même si vous ne prévoyez d’utiliser que GnuPG 2.x, gardez à l’esprit que certains <em>frontends</em> graphiques (comme Enigmail par exemple) font par défaut appel à GnuPG 1.x. Vous devez donc soit modifier la configuration de ces <em>frontends</em>, pour qu’ils appellent <code class="language-plaintext highlighter-rouge">gpg2</code> au lieu de <code class="language-plaintext highlighter-rouge">gpg</code>, soit forcer tout de même l’agent à démarrer au début de la session pour que <code class="language-plaintext highlighter-rouge">gpg</code> puisse le trouver.</p>
|
|||
|
|
|||
|
<p>Une autre raison de forcer le démarrage de l’agent en début de session est si vous prévoyez de l’utiliser comme agent SSH (ce qui est nécessaire pour utiliser la carte OpenPGP pour s’authentifier auprès d’un serveur SSH, comme nous le verrons plus loin) : cela permet de s’assurer que l’agent sera toujours disponible pour les clients SSH (qui ne sont évidemment pas conçus pour lancer un agent GnuPG à la demande).</p>
|
|||
|
|
|||
|
<p><strong>GnuPG 2.1.x</strong></p>
|
|||
|
|
|||
|
<p>L’agent de GnuPG 2.1.x utilise toujours une <em>socket</em> standard et peut donc toujours être lancé à la demande sans qu’aucune configuration particulière ne soit nécessaire.</p>
|
|||
|
|
|||
|
<p>Toutefois, pour les mêmes raisons que ci-dessus, il reste pertinent de forcer le démarrage de l’agent en début de session : pour être sûr que GnuPG 1.x et les programmes SSH soient capables de le trouver quand ils en ont besoin. Pour cela, ajoutez la ligne suivante dans votre fichier <code class="language-plaintext highlighter-rouge">~/.xprofile</code> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg-connect-agent /bye
|
|||
|
export GPG_AGENT_INFO=$HOME/.gnupg/S.gpg-agent:0:1
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>La première ligne force le démarrage de l’agent (<code class="language-plaintext highlighter-rouge">gpg-connect-agent</code> est un utilitaire permettant de communiquer avec l’agent GnuPG ; comme les autres composants de GnuPG 2.1.0, il lance automatiquement un nouvel agent s’il n’en détecte pas un déjà en cours d’exécution). La deuxième définit la variable GPG_AGENT_INFO au bénéfice de GnuPG 1.x (vous pouvez vous en passer si vous ne prévoyez pas du tout d’utiliser GnuPG 1.x, que ce soit directement ou via un <em>frontend</em>).</p>
|
|||
|
|
|||
|
<p>Si vous prévoyez d’utiliser l’agent GnuPG comme agent SSH, ajoutez en plus la ligne suivante, au bénéfice des clients SSH :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> export SSH_AUTH_SOCK=$HOME/.gnupg/S.gpg-agent.ssh
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h2 id="préparer-la-carte">Préparer la carte</h2>
|
|||
|
|
|||
|
<h3 id="changer-les-pin">Changer les PIN</h3>
|
|||
|
|
|||
|
<p>La carte peut vous être fournie soit avec des PIN par défaut, qui sont alors 123456 pour le PW1 et 12345678 pour le PW3, soit avec des PIN personnalisés qui doivent alors vous être communiqués (c’est le cas par exemple si vous obtenez la carte auprès de la Free Software Foundation Europe). Dans tous les cas, vous devriez changer ces PIN au plus tôt.</p>
|
|||
|
|
|||
|
<p>Insérez la carte dans le lecteur et lancez l’éditeur de carte de GnuPG, qui vous accueille en listant le contenu des principaux <em>DO</em> de la carte :</p>
|
|||
|
|
|||
|
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>gpg2 <span class="nt">--card-edit</span>
|
|||
|
Application ID ...: D2760001240102000005000012340000
|
|||
|
Version ..........: 2.0
|
|||
|
Manufacturer .....: ZeitControl
|
|||
|
Serial number ....: 00001234
|
|||
|
Name of cardholder: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
Language prefs ...: de
|
|||
|
Sex ..............: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
URL of public key : <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
Login data .......: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
Signature PIN ....: forced
|
|||
|
Max. PIN lengths .: 32 32 32
|
|||
|
PIN retry counter : 3 0 3
|
|||
|
Signature counter : 0
|
|||
|
Signature key ....: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
Encryption key ...: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
Authentication key: <span class="o">[</span>not <span class="nb">set</span><span class="o">]</span>
|
|||
|
|
|||
|
gpg/card>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>(Lorsque vous voudrez juste afficher le contenu de la carte, vous pourrez obtenir la même sortie avec la commande non-interactive <code class="language-plaintext highlighter-rouge">gpg2 --card-status</code>.)</p>
|
|||
|
|
|||
|
<p>Vous êtes alors dans le menu d’édition de carte de GnuPG, signalé par l’invite <code class="language-plaintext highlighter-rouge">gpg/card></code>. Lorsque vous arrivez dans ce menu, la plupart des commandes d’édition ne sont pas immédiatement disponibles, il faut les demander explicitement avec la commande <code class="language-plaintext highlighter-rouge">admin</code> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg/card> admin
|
|||
|
Admin commands are allowed.
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Vous pouvez à présent accéder au menu relatif aux différents PIN :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg/card> passwd
|
|||
|
1 - change PIN
|
|||
|
2 - unblock PIN
|
|||
|
3 - change Admin PIN
|
|||
|
4 - set the Reset Code
|
|||
|
Q - Quit
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Changez successivement le PIN administrateur (option 3 — le PIN administrateur actuel, celui par défaut, vous sera alors demandé), puis le PIN utilisateur (option 1).</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Attention</strong></p>
|
|||
|
|
|||
|
<p>Souvenez‐vous que que la carte impose une longueur minimale pour chaque code PIN. Un code PIN utilisateur de moins de 6 caractères sera refusé, de même qu’un PIN administrateur de moins de 8 caractères. Les longueurs maximales, non définies par la spécification, sont, quant à elles, indiquées par la ligne <em>Max. PIN lengths</em> dans la sortie ci‐dessus (dans l’ordre PIN utilisateur, <em>Reset Code</em>, PIN administrateur).</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<h3 id="modifier-les-données-de-la-carte">Modifier les données de la carte</h3>
|
|||
|
|
|||
|
<p>Pendant que vous êtes dans le menu d’édition, vous pouvez remplir les différents champs de données concernant le porteur de la carte (je supposerai par la suite que vous vous appelez <em>Alice</em>, par souci de la tradition) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg/card> name
|
|||
|
Cardholder's surname: Smith
|
|||
|
Cardholder's given name: Alice
|
|||
|
|
|||
|
gpg/card> lang
|
|||
|
Language preferences: fr
|
|||
|
|
|||
|
gpg/card> sex
|
|||
|
Sex ((M)ale, (F)emale or space): F
|
|||
|
|
|||
|
gpg/card> url
|
|||
|
URL to retrieve public key: http://example.net/~alice/pgp.asc
|
|||
|
|
|||
|
gpg/card> login
|
|||
|
Login data (account name): alice
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Vous pouvez aussi écrire dans <a href="#les-champs-priv%C3%A9s-ou-%C3%A0-usage-ind%C3%A9fini">les champs privés</a> avec la commande <code class="language-plaintext highlighter-rouge">privatedo</code>, même si celle-ci n’apparaît pas dans le menu de l’éditeur de carte. Par exemple, pour écrire dans le <em>Private DO 1</em> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg/card> privatedo 1
|
|||
|
Private DO data: lorem ipsum dolor sit amet
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Même chose à partir d’un fichier :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg/card> privatedo 1 < fichier
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Quand vous avez modifié ce que vous vouliez, quittez l’éditeur pour revenir à l’invite de commande du shell :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg/card> quit
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="les-clefs-privées">Les clefs privées</h3>
|
|||
|
|
|||
|
<p>Avant de pouvoir utiliser la carte pour des opérations cryptographiques, il faut y stocker des clefs privées.</p>
|
|||
|
|
|||
|
<p>Les clefs peuvent être générées directement sur la carte, auquel cas GnuPG récupère les clefs publiques correspondantes et les importe dans le trousseau public. Attention, dans ce cas la carte contient les <em>seuls</em> exemplaires existants des clefs privées, aucune sauvegarde n’est possible puisque, par conception, on ne peut pas extraire les clefs privées de la carte.</p>
|
|||
|
|
|||
|
<p>L’autre option consiste à générer les clefs, comme d’habitude, sur son ordinateur, puis à importer les clefs privées sur la carte. Elles sont alors supprimées du trousseau privé et remplacées par des <em>stubs</em> dont la seule fonction est d’indiquer à GnuPG que les vraies clefs privées sont sur une carte à puce.</p>
|
|||
|
|
|||
|
<p>C’est cette option que nous allons voir à présent — après tout si vous vous êtes procuré une carte OpenPGP, c’est que vous êtes probablement déjà utilisateur de GnuPG et vous avez donc probablement déjà un jeu de clefs.</p>
|
|||
|
|
|||
|
<p>Dans le reste de ce document, nous utiliserons comme exemple le trousseau suivant :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg2 --list-keys alice
|
|||
|
pub 4096R/6FA9FD8B 2014-08-06 [expires: 2017-08-05]
|
|||
|
uid [ultimate] Alice <alice@example.net>
|
|||
|
sub 2048R/129E3125 2014-08-06 [expires: 2015-08-06]
|
|||
|
sub 2048R/E293CCFC 2014-08-06 [expires: 2015-06_06]
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Nous avons ici une clef maître RSA de 4096 bits et deux sous-clefs RSA de 2048 bits chacune — une de chiffrement et une de signature, même si cela n’apparaît pas dans ce listing. Seules ces deux sous-clefs sont utilisées quotidiennement, la clef maître ne sert qu’à modifier le jeu de clefs (ajouter ou révoquer des identités ou des sous-clefs) ou pour signer les clefs d’autres utilisateurs.</p>
|
|||
|
|
|||
|
<p>##{# Créer une sous-clef d’authentification</p>
|
|||
|
|
|||
|
<p>Nous allons d’abord ajouter une troisième sous-clef RSA de 2048 bits qui servira aux fonctions d’authentification que nous verrons plus loin.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Si vous ne prévoyez pas de faire usage des fonctions d’authentification, vous pouvez passer directement à la section suivante, où nous transférerons toutes les sous-clefs sur la carte. Si vous changez d’avis par la suite, vous pourrez toujours ajouter une sous-clef d’authentification à tout moment.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Lancez l’éditeur de clefs de GnuPG sur votre trousseau (notez l’option <code class="language-plaintext highlighter-rouge">--expert</code> dans la ligne de commande ci-dessous : elle vous donne accès aux options avancées de création de clefs) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg2 --expert --edit-key alice
|
|||
|
Secret key is available.
|
|||
|
|
|||
|
pub 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05 usage: CS
|
|||
|
trust: ultimate validity: ultimate
|
|||
|
sub 2048R/129E3125 created: 2014-08-06 expires: 2015-08-06 usage: E
|
|||
|
sub 2048R/E293CCFC created: 2014-08-06 expires: 2015-08-06 usage: S
|
|||
|
[ultimate] (1). Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Contrairement au précédent, ce listing fait apparaître les rôles attribuées à chaque clef : <code class="language-plaintext highlighter-rouge">E</code> pour le chiffrement (<em>Encrypt</em>), <code class="language-plaintext highlighter-rouge">S</code> pour la signature et <code class="language-plaintext highlighter-rouge">C</code> pour la signature de clefs (<em>Certify</em> — « signer une clef » revient à <em>certifier</em> qu’elle appartient bien à qui elle prétend appartenir).</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg> addkey
|
|||
|
Key is protected
|
|||
|
|
|||
|
You need a passphrase to unlock the secret key for
|
|||
|
user: "Alice <alice@example.net>"
|
|||
|
4096-bit RSA key, ID 6FA9FD8B, created 2014-08-06
|
|||
|
|
|||
|
Please select what kind of key you want:
|
|||
|
(3) DSA (sign only)
|
|||
|
(4) RSA (sign only)
|
|||
|
(5) Elgamal (encrypt only)
|
|||
|
(6) RSA (encrypt only)
|
|||
|
(7) DSA (set your own capabilities)
|
|||
|
(8) RSA (set your own capabilities)
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Choisissez <code class="language-plaintext highlighter-rouge">(8) RSA (set your own capabilities)</code>.</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Possible actions for a RSA key: Sign Encrypt Authenticate
|
|||
|
Current allowed actions: Sign Encrypt
|
|||
|
|
|||
|
(S) Toggle the sign capability
|
|||
|
(E) Toggle the encrypt capability
|
|||
|
(A) Toggle the authenticate capability
|
|||
|
(Q) Finished
|
|||
|
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Choisissez successivement <code class="language-plaintext highlighter-rouge">(S)</code>, <code class="language-plaintext highlighter-rouge">(E)</code> et <code class="language-plaintext highlighter-rouge">(A)</code> pour désactiver les fonctions de signature et de chiffrement et activer la fonction d’authentification, puis <code class="language-plaintext highlighter-rouge">(Q)</code> pour quitter ce sous-menu. Le reste de la procédure est classique pour une génération de sous-clef :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> RSA keys may be between 1024 and 4096 bits long.
|
|||
|
What keysize do you want? (2048)
|
|||
|
Requested keysize is 2048 bits
|
|||
|
Please specify how long the key should be valid.
|
|||
|
0 = keys does not expire
|
|||
|
<n> = key expires in n days
|
|||
|
<n>w = key expires in n weeks
|
|||
|
<n>m = key expires in n months
|
|||
|
<n>y = key expires in n years
|
|||
|
Key is valid for? (0)
|
|||
|
Key expires at Wed 20 Sep 2017 18:31:56 PM CEST
|
|||
|
Is this correct? (y/N)
|
|||
|
Really create? (y/N)
|
|||
|
|
|||
|
We need to generate a lot of random bytes. It is a good idea to perform
|
|||
|
some other actions (type on the keyboard, move the mouse, utilize the
|
|||
|
disks) during the prime generation; this gives the random number
|
|||
|
generator a better chance to gain enough entropy.
|
|||
|
|
|||
|
pub 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05 usage: CS
|
|||
|
trust: ultimate validity: ultimate
|
|||
|
sub 2048R/129E3125 created: 2014-08-06 expires: 2015-08-06 usage: E
|
|||
|
sub 2048R/E293CCFC created: 2014-08-06 expires: 2015-08-06 usage: S
|
|||
|
sub 2048R/EC8E794D created: 2014-09-21 expires: 2017-09-20 usage: A
|
|||
|
[ultimate] (1). Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="transférer-les-sous-clefs-sur-la-carte">Transférer les sous-clefs sur la carte</h3>
|
|||
|
|
|||
|
<p>Nous pouvons à présent transférer les trois sous-clefs sur la carte OpenPGP. Notez qu’il est tout-à-fait possible d’y transférer la clef maître (dans le slot réservé à la clef de signature, puisque la clef maître peut généralement <em>signer</em> en plus de pouvoir <em>certifier</em>), mais cet usage est généralement déconseillé.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Si vous ne l’avez pas déjà fait après avoir créé vos clefs, vous devriez envisager de sauvegarder votre trousseau privé actuel avant de procéder au transfert. Souvenez-vous, le transfert des clefs privées sur la carte <em>les supprime du trousseau sur l’ordinateur</em>. Si vous voulez pouvoir restaurer vos sous-clefs en cas de perte de la carte, vous aurez besoin de cette sauvegarde :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge">
|
|||
|
<div class="highlight"><pre class="highlight"><code>$ gpg2 --armor --output my-private-keys.asc --export-secret-key alice
|
|||
|
</code></pre></div> </div>
|
|||
|
|
|||
|
<p>Stockez le fichier résultant dans un endroit sûr, hors-ligne (au même endroit que là où vous avez stocké le certificat de révocation de la clef maître).</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Assurez-vous que votre carte est dans le lecteur et relancez l’éditeur de clefs (<em>pas</em> l’éditeur de carte — c’est contre-intuitif mais logique : transférer une clef existante sur une carte est une modification du trousseau) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg2 --edit-key alice
|
|||
|
Secret key is available.
|
|||
|
|
|||
|
pub 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05 usage: CS
|
|||
|
trust: ultimate validity: ultimate
|
|||
|
sub 2048R/129E3125 created: 2014-08-06 expires: 2015-08-06 usage: E
|
|||
|
sub 2048R/E293CCFC created: 2014-08-06 expires: 2015-08-06 usage: S
|
|||
|
sub 2048R/EC8E794D created: 2014-09-21 expires: 2017-09-20 usage: A
|
|||
|
[ultimate] (1). Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Basculez en mode d’affichage des clefs privées :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg> toggle
|
|||
|
sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Sélectionnez la première sous-clef (la clef de chiffrement)…</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg> key 1
|
|||
|
sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb* 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>… et envoyez-là vers la carte à puce :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg> keytocard
|
|||
|
Signature key ....: [none]
|
|||
|
Encryption key ...: [none]
|
|||
|
Authentication key: [none]
|
|||
|
|
|||
|
Please select where to store the key:
|
|||
|
(2) Encryption key
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>La première sous-clef n’ayant que la capacité de chiffrer, vous n’avez pas d’autre choix ici que de la stocker dans le slot réservé à la clef de chiffrement.</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> You need a passphrase to unlock the secret key for
|
|||
|
user: "Alice <alice@example.net>"
|
|||
|
2048-bit RSA key, ID 129E3125, created 2014-08-06
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Saisissez comme demandé la phrase de passe qui protège actuellement votre clef privée sur votre disque dur.</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb* 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
card no: 0005 00001234
|
|||
|
ssb 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Notez la mention <code class="language-plaintext highlighter-rouge">card no:</code> qui indique que la sous-clef se trouve sur la carte portant le numéro de série <code class="language-plaintext highlighter-rouge">0005 00001234</code>.</p>
|
|||
|
|
|||
|
<p>Déselectionnez la première sous-clef puis répétez la procédure avec la seconde (la clef de signature) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gpg> key 1
|
|||
|
sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
card no: 0005 00001234
|
|||
|
ssb 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
|
|||
|
gpg> key 2
|
|||
|
sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
card no: 0005 00001234
|
|||
|
ssb* 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
|
|||
|
gpg> keytocard
|
|||
|
Signature key ....: [none]
|
|||
|
Encryption key ...: 934E E224 AF04 3996 6264 B4D1 7DAC 67E9 129E 3125
|
|||
|
Authentication key: [none]
|
|||
|
|
|||
|
Please select where to store the key:
|
|||
|
(1) Signature key
|
|||
|
(3) Authentication key
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Stockez cette clef dans le slot <code class="language-plaintext highlighter-rouge">(1)</code>, réservé à la clef de signature. Le slot réservé à la clef d’authentification vous est aussi proposé car une clef capable de signer peut aussi servir à authentifier (l’authentification est un cas particulier de signature, où vous signez un défi posé par celui qui vous demande de vous authentifier).</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> You need a passphrase to unlock the secret key for
|
|||
|
user: "Alice <alice@example.net>"
|
|||
|
2048-bit RSA key, ID E293CCFC, created 2014-08-06
|
|||
|
|
|||
|
sec 4096R/6FA9FD8B created: 2014-08-06 expires: 2017-08-05
|
|||
|
ssb 2048R/129E3125 created: 2014-08-06 expires: never
|
|||
|
card no: 0005 00001234
|
|||
|
ssb* 2048R/E293CCFC created: 2014-08-06 expires: never
|
|||
|
card no: 0005 00001234
|
|||
|
ssb 2048R/EC8E794D created: 2014-09-21 expires: never
|
|||
|
(1) Alice <alice@example.net>
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Répétez une dernière fois la procédure pour transférer la sous-clef d’authentification dans le slot prévu à cet effet, puis, quittez l’éditeur de clefs <em>en enregistrant vos modifications</em> — sinon les sous-clefs auront bien été transférées sur la carte, mais ne seront pas effacées de votre trousseau privé sur votre disque dur !</p>
|
|||
|
|
|||
|
<h2 id="signer-ou-déchiffrer-des-messages-openpgp">Signer ou déchiffrer des messages OpenPGP</h2>
|
|||
|
|
|||
|
<p>Le fait que vos clefs privées soient désormais sur une carte à puce ne change à peu près rien à votre manière d’utiliser GnuPG tous les jours pour signer ou déchiffrer des messages OpenPGP. Et ce, indépendamment du programme que vous utilisez en avant-plan (que ce soit GnuPG directement en ligne de commande, un <em>frontend</em> graphique comme GNU Privacy Assistant, un greffon pour client de messagerie comme Enigmail, etc.).</p>
|
|||
|
|
|||
|
<p>Au moment de signer ou déchiffrer un message, l’agent GnuPG, au lieu de vous demander la phrase de passe protégeant le trousseau privé, vous demandera d’insérer votre carte dans le lecteur si elle n’y est pas déjà, puis vous demandera le PIN. En arrière-plan, le fonctionnement de GnuPG sera quelque peu différent (puisqu’il devra déléguer une partie du travail à la carte au lieu de le faire lui-même), mais ça ne sera pas visible pour vous (à part un éventuel clignotement de la diode témoin d’activité de votre lecteur de carte).</p>
|
|||
|
|
|||
|
<p>Le seul changement important réside dans le fait qu’une fois votre PIN saisi une première fois, il ne vous sera plus jamais demandé tant que vous ne retirez pas la carte du lecteur. Inutile d’aller jouer avec l’option <code class="language-plaintext highlighter-rouge">--default-cache-ttl</code> de l’agent GnuPG, ce n’est pas lui qui garde votre PIN en cache : c’est la carte OpenPGP elle-même qui reste dans un état « PIN vérifié » tant que la connexion entre la carte et Scdaemon est maintenue.</p>
|
|||
|
|
|||
|
<p>Pour contraindre ponctuellement la carte à vous redemander le PIN, il faut provoquer une réinitialisation de la connexion, soit en retirant physiquement la carte du lecteur, soit en envoyant une commande <code class="language-plaintext highlighter-rouge">SCD RESET</code> à l’agent GnuPG :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg-connect-agent 'SCD RESET' /bye
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="le-mode-pin-forcé-pour-les-signatures">Le mode PIN forcé pour les signatures</h3>
|
|||
|
|
|||
|
<p>La carte peut être configurée pour exiger <em>systématiquement</em> le PIN avant toute opération utilisant la clef de signature, indépendamment du fait que le PIN a déjà été vérifié ou non. C’est le mode <em>forced PIN</em>, dont l’état (enclenché ou non) est visible lorsque GnuPG affiche le contenu de la carte :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gpg2 --card-edit
|
|||
|
[…]
|
|||
|
Signature PIN ....: not forced
|
|||
|
[…]
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Ici, la carte est en mode normal, le PIN ne sera exigé que s’il n’a pas encore été vérifié depuis l’insertion de la carte. La commande <code class="language-plaintext highlighter-rouge">forcesig</code> permet de basculer entre le mode normal et le mode « PIN forcé » :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg/card> forcesig
|
|||
|
|
|||
|
gpg/card> list
|
|||
|
[…]
|
|||
|
Signature PIN ....: forced
|
|||
|
[…]
|
|||
|
|
|||
|
gpg/card> quit
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Cette option ne concerne que les opérations de signature. Pour le déchiffrement ou l’authentification, le PIN ne sera jamais exigé s’il a déjà été vérifié une première fois et que la carte n’a pas été réinitialisée entre-temps.</p>
|
|||
|
|
|||
|
<h2 id="authentification-sur-un-serveur-ssh">Authentification sur un serveur SSH</h2>
|
|||
|
|
|||
|
<p>Dans ce que j’appelle les « usages avancés » de la carte OpenPGP, l’authentification SSH est probablement l’un des plus intéressants.</p>
|
|||
|
|
|||
|
<p>Il faut pour cela remplacer l’agent SSH fourni en standard avec OpenSSH (<code class="language-plaintext highlighter-rouge">ssh-agent</code>) par l’agent GnuPG. Ajoutez simplement l’option <code class="language-plaintext highlighter-rouge">enable-ssh-support</code> dans le fichier de configuration de l’agent GnuPG <code class="language-plaintext highlighter-rouge">$GNUPGHOME/gpg-agent.conf</code>. Puis, comme évoqué <a href="#lagent-gnupg">ci-avant</a>, assurez-vous que ledit agent est bien démarré systématiquement en début de session et que la variable d’environnement SSH_AUTH_SOCK est définie et pointe vers la <em>socket</em> spécialement créée à l’intention des clients SSH.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Si vous utilisiez précédemment <code class="language-plaintext highlighter-rouge">ssh-agent</code>, assurez-vous aussi qu’il n’est <em>plus</em> démarré au début de votre session.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>L’agent GnuPG s’utilise comme l’agent SSH standard et vous pouvez toujours interagir avec lui avec l’outil classique <code class="language-plaintext highlighter-rouge">ssh-add</code>. Vos clefs SSH pré-existantes sont toujours utilisables de la même façon qu’auparavant. Par exemple, pour charger votre clef par défaut (<code class="language-plaintext highlighter-rouge">~/.ssh/id_rsa</code>) dans l’agent, procédez comme d’habitude :</p>
|
|||
|
|
|||
|
<p>Pour utiliser la clef d’authentification que vous avez générée un peu plus tôt et transférée sur votre carte OpenPGP, vous n’avez rien d’autre à faire que d’insérer la carte dans le lecteur : l’agent GnuPG détectera automatiquement la présence d’une clef d’authentification et la rendra accessible aux clients SSH. Vous pouvez le vérifier en demandant à l’agent de lister les « identités » disponibles :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh-add -l
|
|||
|
2048 77:e5:34:2f:8d:1c:7a:de:89:b5:13:a2:f1:2c:77:bf cardno:000500001234 (RSA)
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Il ne vous reste plus qu’à exporter votre clef publique sous une forme utilisable dans un fichier <code class="language-plaintext highlighter-rouge">~/.ssh/authorized_keys</code>, ce que vous pouvez faire soit avec <code class="language-plaintext highlighter-rouge">ssh-add</code> encore (seulement quand la carte est dans le lecteur) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh-add -L
|
|||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWuoVgdIK8pmN5DJl5kymyI+em+uIfzPCuls7DVOfd
|
|||
|
C2oz0sXkuvdqMvIGmUW80uRFWetdXO2ltF78dABPjUGtfXUdjjsD6WEuP6j0JeH2w2l2j7f3icwDVmNN
|
|||
|
OdwwE6sFmW0YgNNPRr5leGaoA9OMo5klF5BTyn2iFCgK0QV3zL1pk7E3x8oSA0COC2Jn0P5Pc2foEqSn
|
|||
|
Dtaq+KTJKA3Sng5R+khWL6Ux5/F7VKPOzIBaJm+wQ16LW4pitVbPMYxrNWO44X+2GpJ6IXBw/ixZK8rE
|
|||
|
qysSPJz7jfUZ8HPJvAnnEwHWOpYfksZU/dXTnQ4C2btpMDZeoQsnRnxTx4Mh cardno:000500001234
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Soit avec l’outil (non-documenté) <code class="language-plaintext highlighter-rouge">gpgkey2ssh</code>, qui prend en paramètre
|
|||
|
l’identifiant de la sous-clef d’authentification :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpgkey2ssh EC8E794D
|
|||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWuoVgdIK8pmN5DJl5kymyI+em+uIfzPCuls7DVOfd
|
|||
|
C2oz0sXkuvdqMvIGmUW80uRFWetdXO2ltF78dABPjUGtfXUdjjsD6WEuP6j0JeH2w2l2j7f3icwDVmNN
|
|||
|
OdwwE6sFmW0YgNNPRr5leGaoA9OMo5klF5BTyn2iFCgK0QV3zL1pk7E3x8oSA0COC2Jn0P5Pc2foEqSn
|
|||
|
Dtaq+KTJKA3Sng5R+khWL6Ux5/F7VKPOzIBaJm+wQ16LW4pitVbPMYxrNWO44X+2GpJ6IXBw/ixZK8rE
|
|||
|
qysSPJz7jfUZ8HPJvAnnEwHWOpYfksZU/dXTnQ4C2btpMDZeoQsnRnxTx4Mh COMMENT
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Une fois la clef publique connue du serveur, vous pouvez vous y connecter comme d’habitude, avec n’importe quelle commande SSH (<code class="language-plaintext highlighter-rouge">ssh</code>, <code class="language-plaintext highlighter-rouge">scp</code>, etc.) ou n’importe quel programme utilisant SSH en arrière-plan (<code class="language-plaintext highlighter-rouge">git push</code> par exemple). L’agent GnuPG vous demandera votre PIN si nécessaire ; s’il a déjà été vérifié plus tôt, vous serez automatiquement connecté sans avoir à saisir quoi que ce soit.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Remarquez que du point de vue du serveur SSH, il ne se passe rien de spécial. Tout ce qu’il voit est une banale authentification par clef — le fait que la partie privée de la clef soit, côté client, stockée sur une carte à puce, est indifférent pour le serveur. Tout ce qui lui importe est que la partie publique figure dans la liste des clefs autorisées.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<h2 id="utiliser-un-certificat-x509">Utiliser un certificat X.509</h2>
|
|||
|
|
|||
|
<p>À partir de chacune des clefs privées de la carte OpenPGP, il est possible de créer un <a href="http://tools.ietf.org/html/rfc5280">certificat X.509</a>. Un tel certificat permet d’utiliser la carte pour, entre autres usages, s’authentifier auprès d’un serveur TLS, signer ou chiffrer des messages S/MIME, ou encore signer des documents OpenDocument.</p>
|
|||
|
|
|||
|
<h3 id="créer-un-certificat-x509">Créer un certificat X.509</h3>
|
|||
|
|
|||
|
<p>GnuPG 2.x fournit l’outil <code class="language-plaintext highlighter-rouge">gpgsm</code> pour créer, manipuler et utiliser des certificats X.509. Mettez votre carte dans le lecteur et appelez <code class="language-plaintext highlighter-rouge">gpgsm</code> pour créer une nouvelle demande de certificat :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpgsm --armor --output certificat.csr --gen-key
|
|||
|
Please select what kind of key you want:
|
|||
|
(1) RSA
|
|||
|
(2) Existing key
|
|||
|
(3) Existing key from card
|
|||
|
Your selection?
|
|||
|
|
|||
|
Serial number of the card: D2760001240102000005000012340000
|
|||
|
Available keys:
|
|||
|
(1) 39820691E60A775AF9B979F4A960B23A2FC8892A OPENPGP.1
|
|||
|
(2) BE3918CCCC237E42AF6D15869DCAE291276C5548 OPENPGP.2
|
|||
|
(3) 386F81432E2C864085885251EB5D6D0B875D1E91 OPENPGP.3
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Les empreintes affichées ici ne correspondent <em>pas</em> aux empreintes traditionnelles de GnuPG (celles que <code class="language-plaintext highlighter-rouge">gpg2</code> affiche avec l’option <code class="language-plaintext highlighter-rouge">--fingerprint</code>) : ce sont des <em>keygrips</em>. Un <em>keygrip</em> est calculé sur une clef « brute » et permet donc d’identifier une clef indépendamment du format du certificat qui la contient — un certificat OpenPGP et un certificat X.509 utilisant la même clef auront deux empreintes différentes, mais le même <em>keygrip</em>.</p>
|
|||
|
|
|||
|
<p>Malheureusement, avec GnuPG 2.0.x, il n’y a aucun moyen simple à ma connaissance d’obtenir le <em>keygrip</em> d’une clef OpenPGP et donc de savoir à quelle clef correspondent chacun des <em>keygrips</em> ci-dessus (GnuPG 2.1 a une option <code class="language-plaintext highlighter-rouge">--with-keygrip</code> pour ça). À la place, vous devez vous référer aux noms des <em>slots</em> de la carte OpenPGP, sachant que le premier (<code class="language-plaintext highlighter-rouge">OPENPGP.1</code>) correspond à la clef de signature, le second à la clef de chiffrement et le troisième à la clef d’authentification.</p>
|
|||
|
|
|||
|
<p>Le choix de la clef conditionnera les usages possibles du futur certificat : il sera utilisable seulement pour signer avec la clef OPENPGP.1, seulement pour chiffrer avec la clef OPENPGP.2, pour signer et s’authentifier avec la clef OPENPGP.3. Il n’est pas possible d’utiliser la carte pour produire un certificat utilisable à la fois pour signer/authentifier <em>et</em> pour chiffrer.</p>
|
|||
|
|
|||
|
<p>Dans cet exemple, nous choisirons la troisième clef pour obtenir un certificat utilisable pour la signature et l’authentification.</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Possible actions for a RSA key:
|
|||
|
(1) sign, encrypt
|
|||
|
(2) sign
|
|||
|
(3) encrypt
|
|||
|
Your selection?
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>La clef d’authentification ne permet pas de chiffrer, donc la seule option pertinente ici est la seconde, <code class="language-plaintext highlighter-rouge">(2) sign</code>. Ce menu ne sert qu’à déterminer la valeur de l’extension <em>X509v3 Key Usage</em> de la demande de certificat — même si vous choisissez <code class="language-plaintext highlighter-rouge">(1) sign, encrypt</code>, ça ne changera rien au fait que la clef sous-jacente ne permet <em>pas</em> de chiffrer, même si le certificat proclamera le contraire.</p>
|
|||
|
|
|||
|
<p>Vous devez ensuite renseigner le sujet du futur certificat :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Enter the X.509 subject name: CN=alice,O=Example Corp,DC=example,DC=net
|
|||
|
Enter email addresses (end with an empty line):
|
|||
|
alice@example.net
|
|||
|
|
|||
|
Enter DNS names (optional; end with an empty line):
|
|||
|
|
|||
|
Enter URIs (optional; end with an empty line):
|
|||
|
|
|||
|
Parameters to be used for the certificate request:
|
|||
|
Key-Type: card:OPENPGP.3
|
|||
|
Key-Length: 1024
|
|||
|
Key-Usage: sign
|
|||
|
Name-DN: CN=Alice,O=Example Corp,DC=example,DC=net
|
|||
|
Name-Email: alice@example.net
|
|||
|
|
|||
|
Really create request? (y/N)
|
|||
|
Now creating certificate request. This may take a while ...
|
|||
|
gpgsm: about to sign CSR for key: &386F81432E2C864085885251EB5D6D0B875D1E91
|
|||
|
gpgsm: certificate request created
|
|||
|
Ready. You should now send this request to your CA.
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Vous récupérez la demande de certificat dans le fichier <code class="language-plaintext highlighter-rouge">certificat.csr</code>. Vous pouvez si vous le souhaitez l’examiner avec, par exemple, <code class="language-plaintext highlighter-rouge">openssl req -in -text</code>.</p>
|
|||
|
|
|||
|
<p>Il vous faut maintenant faire signer cette demande par une autorité de certification. Le choix de l’autorité de certification à laquelle vous adresser dépend grandement de l’usage que vous voulez faire du certificat. Il peut s’agir d’une autorité générique « reconnue » (probablement le meilleur choix pour signer des messages S/MIME à destination de n’importe qui, puisque tout le monde fait confiance — à tort ou à raison — à ces autorités), d’une autorité spécifique à votre institution ou à votre compagnie (pour signer des messages internes à cette institution ou compagnie), ou même de votre propre autorité (par exemple pour vous authentifier auprès de votre propre serveur TLS).</p>
|
|||
|
|
|||
|
<p>Une fois en possession de votre certificat signé, importez-le dans le trousseau de <code class="language-plaintext highlighter-rouge">gpgsm</code> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpgsm --import certificat.crt
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Vous devriez aussi importer le certificat racine de l’autorité qui l’a signé, ainsi que les éventuels certificats intermédiaires. Finalement, testez votre nouveau certificat en signant un fichier quelconque :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpgsm --output quelconque.signed --sign quelconque
|
|||
|
gpgsm: signature created
|
|||
|
$ gpgsm --verify quelconque.signed
|
|||
|
gpgsm: Signature made 2014-12-01 12:11:32 using certificate ID 0xAB4057B0
|
|||
|
gpgsm: Good signature from "/CN=Alice/O=Example Corp/DC=example/DC=net"
|
|||
|
gpgsm: aka "alice@example.net"
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="authentification-auprès-dun-serveur-web">Authentification auprès d’un serveur web</h3>
|
|||
|
|
|||
|
<p>Je supposerai ici que vous voulez accéder à un serveur web qui exige une authentification du client par certificat et que vous souhaitez utiliser, pour cela, le certificat fraîchement obtenu ci-dessus.</p>
|
|||
|
|
|||
|
<p>Je ne traiterai pas de la mise en place de l’authentification côté serveur, qui sort du cadre de ce document (et qui est de toute façon assez bien documentée ailleurs). Je me contenterai de rappeler que les deux lignes suivantes, dans la configuration d’un serveur Apache httpd 2.2.x, obligent un client à présenter un certificat signé par la clef privé du certificat <code class="language-plaintext highlighter-rouge">/etc/ssl/certs/client_ca.pem</code> pour être autorisé à accéder au serveur :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> SSLCACertificateFile /etc/ssl/certs/client_ca.pem
|
|||
|
SSLVerifyClient require
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>En ces temps où <a href="http://arstechnica.com/security/2012/08/passwords-under-assault/">les mots de passe sont de plus en plus mis à mal</a>, l’authentification par certificat (avec ou sans carte à puce) est une méthode de contrôle d’accès très intéressante et probablement trop peu utilisée. Combinée avec l’<a href="http://httpd.apache.org/docs/2.2/mod/mod_ssl.html#ssloptions">option <code class="language-plaintext highlighter-rouge">FakeBasicAuth</code> du module mod_ssl</a>, elle peut remplacer complètement l’authentification par login et mot de passe.</p>
|
|||
|
|
|||
|
<p>Similairement à ce que nous avons vu plus haut pour SSH, le serveur est indifférent au fait que le certificat que vous allez lui présenter a sa clef privée sur une carte à puce, il ne voit qu’un certificat X.509 comme un autre. C’est du côté du client que les choses intéressantes se passent.</p>
|
|||
|
|
|||
|
<p>Côté client, donc, vous devez avoir fait signer votre requête de certificat par l’autorité appropriée et avoir importé le certificat signé dans le trousseau de <code class="language-plaintext highlighter-rouge">gpgsm</code>. Ensuite, il vous faut installer <a href="http://www.scute.org/">Scute</a> et configurer Firefox pour qu’il charge dynamiquement cette bibliothèque (la procédure complète, captures d’écrans à l’appui, est détaillée dans <a href="http://www.scute.org/scute.html/Application-Configuration.html#Application-Configuration">la documentation de Scute</a>).</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>En principe, Scute devrait aussi être utilisable par n’importe quelle application capable d’utiliser un module <a href="http://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-11-cryptographic-token-interface-standard.htm">PKCS #11</a> (Chromium par exemple). Mais je n’ai testé que Firefox, LibreOffice et Thunderbird (voir plus bas).</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Sitôt la carte OpenPGP insérée dans le lecteur, Scute rendra votre certificat disponible pour Firefox — vous pourrez le vérifier en allant constater sa présence dans le <em>Certificate Manager</em> de Firefox. En vous connectant au serveur exigeant un certificat client, Firefox sélectionnera automatiquement ce certificat<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> et vous serez invité à saisir votre PIN (si nécessaire) pour signer une partie de la poignée de main TLS, prouvant ainsi au serveur que vous possédez la clef privée du certificat.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Attention</strong></p>
|
|||
|
|
|||
|
<p>Actuellement, Scute (version 1.4.0) ne fonctionne pas avec TLS 1.2, dont la prise en charge a récemment été introduite dans Firefox à partir de sa version 27. En effet, TLS 1.2 a changé, entre autres choses, la nature du <em>message digest</em> que le client est supposé signer avec sa clef privée : avec TLS <= 1.1, c’était systématiquement un double condensat MD5 et SHA-1 ; avec TLS 1.2, c’est maintenant un condensat variable (SHA-1 ou SHA-256, le plus souvent) enveloppé dans une structure ASN.1. Ce changement a cassé le code au sein de Scute chargé de relayer le <em>message digest</em> à la carte OpenPGP.</p>
|
|||
|
|
|||
|
<p>Malheureusement, le seul contournement aisé pour l’instant est de… désactiver TLS 1.2, ce qui se fait en réglant la variable <code class="language-plaintext highlighter-rouge">security.tls.version.max</code> à 2 dans about:config.</p>
|
|||
|
|
|||
|
<p>Pour ceux qui sont prêts à mettre un peu les mains dans le cambouis, Werner Koch a proposé <a href="http://lists.gnupg.org/pipermail/gnupg-devel/2014-September/028750.html">un patch</a> contre Scute 1.4.0 apportant la prise des <em>message digests</em> utilisés par TLS 1.2. Si vous utilisez GnuPG 2.0.26, ce n’est toutefois pas suffisant, il faut aussi appliquer <a href="http://lists.gnupg.org/pipermail/gnupg-devel/2014-September/028759.html">un patch de votre serviteur</a></p>
|
|||
|
|
|||
|
<p>Par ailleurs, si vous utilisez GnuPG 2.1.0, cette version a révélé un bug de Scute qui conduit l’authentification à échouer dans certaines occasions. J’ai proposé <a href="http://lists.gnupg.org/pipermail/gnupg-devel/2014-December/029175.html">un second patch</a> contre Scute (à appliquer en plus de celui de Werner Koch) qui semble corriger le problème.</p>
|
|||
|
|
|||
|
<p>La prudence s’impose bien sûr avant d’appliquer ces correctifs. On ne patche pas des programmes ou bibliothèques cryptographiques à la légère (il y en a chez Debian qui ont essayé…).</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<h3 id="signer-des-documents-opendocument">Signer des documents OpenDocument</h3>
|
|||
|
|
|||
|
<p>Si vous avez installé Scute et configuré Firefox pour l’utiliser comme expliqué dans la section précédente, vous pouvez également utiliser votre certificat X.509 pour apposer une signature électronique à vos documents OpenDocument depuis LibreOffice.</p>
|
|||
|
|
|||
|
<p>Il suffit pour cela de définir la variable d’environnement MOZILLA_CERTIFICATE_FOLDER et de la faire pointer vers le dossier de votre profil Firefox :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> export MOZILLA_CERTIFICATE_FOLDER=~/.mozilla/firefox/nom_du_profil
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Dès lors, LibreOffice peut utiliser tous les certificats présents dans le Certificate Manager de Firefox, y compris celui que vous avez généré à partir de votre carte OpenPGP.</p>
|
|||
|
|
|||
|
<p>Pour signer un document, rien de plus simple : insérez votre carte dans le lecteur et dans LibreOffice, allez dans <code class="language-plaintext highlighter-rouge">File</code> → <code class="language-plaintext highlighter-rouge">Digital Signatures</code>). Choisissez <code class="language-plaintext highlighter-rouge">Sign Document…</code>, sélectionnez votre certificat, entrez votre PIN, si nécessaire.</p>
|
|||
|
|
|||
|
<h3 id="signer-des-messages-smime">Signer des messages S/MIME</h3>
|
|||
|
|
|||
|
<p>S/MIME (<a href="https://tools.ietf.org/html/rfc3851">RFC 3851</a>) est, avec OpenPGP, l’autre standard proposant la confidentialité et l’authentification « de bout en bout » des messages.</p>
|
|||
|
|
|||
|
<p>Thunderbird prend en charge nativement S/MIME, mais ne permet pas l’utilisation d’une clef privée sur carte à puce. Comme pour Firefox, c’est la bibliothèque Scute qui va lui apporter cette fonctionnalité.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Scute ne permet la signature de messages S/MIME que si les patches évoqués plus haut sont appliqués.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Installez Scute sous Thunderbird de la même manière que sous Firefox (dans les préférences, allez dans la section <code class="language-plaintext highlighter-rouge">Advanced</code>, onglet <code class="language-plaintext highlighter-rouge">Certificates</code>, <code class="language-plaintext highlighter-rouge">Security Devices</code> ; dans le « Device Manager », ajoutez un nouveau module PKCS #11 et spécifiez le chemin complet vers la bibliothèque <code class="language-plaintext highlighter-rouge">libscute.so</code>). Une fois la carte dans le lecteur, votre certificat devrait apparaître dans l’onglet <code class="language-plaintext highlighter-rouge">Your Certificates</code> du « Certificate Manager ».</p>
|
|||
|
|
|||
|
<p>Dans les paramètres de votre compte de messagerie, visitez la section <code class="language-plaintext highlighter-rouge">Security</code> (attention à ne pas confondre avec la section <code class="language-plaintext highlighter-rouge">OpenPGP Security</code>, présente si vous utilisez Enigmail et qui, comme son nom l’indique, concerne OpenPGP et non S/MIME) et sélectionnez votre certificat X.509 pour signer vos messages. Cochez éventuellement la case « Digitally sign messages (by default) », si vous souhaitez systématiquement signer vos messages sortant (ce réglage est toujours ponctuellement désactivable au cas par cas).</p>
|
|||
|
|
|||
|
<p>Votre certificat X.509 est également utilisable à partir de Mutt, à condition que ce dernier utilise la bibliothèque gpgme (GnuPG Made Easy) en lieu et place d’OpenSSL, comme <em>backend</em> cryptographique (en ajoutant l’option <code class="language-plaintext highlighter-rouge">set crypt_use_gpgme</code> dans le fichier de configuration de Mutt). Par l’intermédiaire de cette bibliothèque, Mutt déléguera les opérations de signature S/MIME à gpgsm qui, à son tour, fera appel à la carte OpenPGP.</p>
|
|||
|
|
|||
|
<h3 id="utiliser-la-carte-comme-token-pkcs-15">Utiliser la carte comme token PKCS 15</h3>
|
|||
|
|
|||
|
<p>Dans tous les cas d’utilisation du certificat X.509 présentés ci-dessus, la carte OpenPGP ne contient rien d’autre que la <em>clef privée</em> associée au certificat. Le certificat proprement dit est stocké dans le trousseau de GpgSM, où les applications doivent aller le chercher soit en appelant directement <code class="language-plaintext highlighter-rouge">gpgsm</code>, soit par l’intermédiaire de Scute.</p>
|
|||
|
|
|||
|
<p>Il est toutefois possible de stocker le certificat sur la carte OpenPGP elle-même, ce qui le rend utilisable indépendamment de GpgSM. En fait, cela transforme <em>de facto</em> la carte en un <em>token</em> <a href="http://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-15-cryptographic-token-information-format.htm">PKCS #15</a>, utilisable avec n’importe quel système ou application compatible avec ce format.</p>
|
|||
|
|
|||
|
<p>Le certificat doit être au format DER pour pouvoir être importé sur la carte. Si vous l’aviez rangé dans le trousseau de GpgSM, la commande suivante le sortira directement dans ce format :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpgsm -o alice.der --export alice@example.net
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Si votre autorité de certification vous a remis un certificat au format PEM, vous pouvez utiliser OpenSSL (par exemple) pour le convertir en DER :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ openssl x509 -inform PEM -in alice.pem -outform DER -out alice.der
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Lancez l’éditeur de carte de GnuPG pour procéder au transfert du certificat sur la carte. Notez que la commande <code class="language-plaintext highlighter-rouge">writecert</code> n’apparaît pas dans l’aide en ligne de l’éditeur.</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg2 --card-edit
|
|||
|
|
|||
|
Application ID ...: D27600012301020000005000012340000
|
|||
|
[…]
|
|||
|
|
|||
|
gpg/card> admon
|
|||
|
Admin commands are allowed
|
|||
|
|
|||
|
gpg/card> writecert 3 < alice.der
|
|||
|
|
|||
|
gpg/card> quit
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Note</strong></p>
|
|||
|
|
|||
|
<p>Le premier argument de <code class="language-plaintext highlighter-rouge">writecert</code> est supposé indiquer le slot sur la carte dans lequel enregistrer le certificat. Toutefois, la carte OpenPGP ne possède qu’un seul slot pour certificat, et cet argument doit toujours être <code class="language-plaintext highlighter-rouge">3</code>.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<p>Votre carte <del>OpenPGP</del> PKCS #15 est désormais utilisable de manière autonome et ne requiert plus la présence des composants de GnuPG sur la machine. Toute application prenant en charge PKCS #15 (par exemple par l’intermédiaire d’<a href="https://github.com/OpenSC/OpenSC/wiki">OpenSC</a>) peut exploiter la carte.</p>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Attention</strong></p>
|
|||
|
|
|||
|
<p>L’utilisation simultanée de la carte avec GnuPG et comme <em>token</em> PKCS #15 est impossible. En effet, Scdaemon requiert un accès <em>exclusif</em> à la carte, interdisant son utilisation par d’autres programmes comme OpenSC. C’est d’ailleurs la raison d’être de Scute : permettre à des programmes indépendants de GnuPG d’accèder à la carte via une interface standard (PKCS #11) fonctionnant au-dessus de Scdaemon, et laisser ce dernier être le seul à exploiter la carte directement.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<h2 id="miscellanées">Miscellanées</h2>
|
|||
|
|
|||
|
<h3 id="communiquer-avec-scdaemon-et-la-carte">Communiquer avec Scdaemon et la carte</h3>
|
|||
|
|
|||
|
<p>Scdaemon, le démon auxiliaire de GnuPG 2.x gérant les cartes à puce, travaille normalement dans l’ombre des autres composants de GnuPG et l’utilisateur n’a jamais affaire à lui.</p>
|
|||
|
|
|||
|
<p>Il est néanmoins possible de dialoguer avec Scdaemon indépendamment des <em>frontend</em> de GnuPG. Cela peut occasionnellement être utile à des fins de débogage, pour d’éventuels usages avancés non prévus par les <em>frontend</em>, ou simplement par curiosité, pour comprendre ce qui se passe en arrière-plan.</p>
|
|||
|
|
|||
|
<p>Comme l’agent GnuPG, Scdaemon écoute sur une <em>socket</em> Unix et utilise le protocole <a href="https://www.gnupg.org/documentation/manuals/assuan/index.html">Assuan</a> pour recevoir ses commandes.</p>
|
|||
|
|
|||
|
<p>Avec GnuPG 2.1.0, la <em>socket</em> de Scdaemon est constante et fixée à <code class="language-plaintext highlighter-rouge">$GNUPGHOME/S.scdaemon</code>. On peut ainsi contacter le démon de la manière suivante :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ echo SERIALNO | socat - unix-connect:/home/alice/.gnupg/S.scdaemon
|
|||
|
OK GNU Privacy Guard's Smartcard server ready
|
|||
|
S SERIALNO D276000240102000005000012340000 0
|
|||
|
OK
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>(La commande <code class="language-plaintext highlighter-rouge">SERIALNO</code> demande à Scdaemon le numéro de la série de la carte OpenPGP actuellement insérée dans le lecteur.)</p>
|
|||
|
|
|||
|
<p>Mais il est probablement plus simple de passer par l’agent GnuPG (et son utilitaire <code class="language-plaintext highlighter-rouge">gpg-connect-agent</code>), qui fournit une commande <code class="language-plaintext highlighter-rouge">SCD</code> pour transmettre des commandes à Scdaemon. Cela permet de contacter Scdaemon sans avoir à se soucier de l’emplacement de sa <em>socket</em> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg-connect-agent 'SCD SERIALNO' /bye
|
|||
|
S SERIALNO D276000240102000005000012340000 0
|
|||
|
OK
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Une fois qu’on sait communiquer avec Scdaemon, on peut obtenir la liste des commandes acceptées avec la commande <code class="language-plaintext highlighter-rouge">HELP</code>, et l’aide d’une commande particulière avec <code class="language-plaintext highlighter-rouge">HELP commande </code> :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gpg-connect-agent
|
|||
|
> SCD HELP
|
|||
|
[…]
|
|||
|
# SERIALNO [<apptype>]
|
|||
|
# LEARN [--force] [--keypairinfo]
|
|||
|
# READCERT <hexified_certid>|<keyid>
|
|||
|
# READKEY <keyid>
|
|||
|
# SETDATA [--append] <hexstring>
|
|||
|
# PKSIGN [--hash=[rmd160|sha{1,224,256,384,512}|md5]] <hexified_id<
|
|||
|
# PKAUTH <hexified_id>
|
|||
|
# PKDECRYPT <hexified_id>
|
|||
|
# INPUT
|
|||
|
# OUTPUT
|
|||
|
# GETATTR <name>
|
|||
|
# SETATTR <name> <value>
|
|||
|
# WRITECERT <hexified_certid>
|
|||
|
# WRITEKEY [--force] <keyid>
|
|||
|
# GENKEY [--force] [--timestamp=<isodate>] <no>
|
|||
|
# RANDOM <nbytes>
|
|||
|
# PASSWD [--reset] [--nullpin] <chvno>
|
|||
|
# CHECKPIN <idstr>
|
|||
|
# LOCK [--wait]
|
|||
|
# UNLOCK
|
|||
|
# GETINFO <what>
|
|||
|
# RESTART
|
|||
|
# DISCONNECT
|
|||
|
# APDU [--[dump-]attr] [--more] [--exlen[=N]] [hexstring]
|
|||
|
# KILLSCD
|
|||
|
OK
|
|||
|
> SCD HELP READKEY
|
|||
|
# READKEY <keyid>
|
|||
|
#
|
|||
|
# Return the public key for given cert or key ID as a standard
|
|||
|
# S-expression.
|
|||
|
#
|
|||
|
# Note, that this function may even be used on a locked card.
|
|||
|
OK
|
|||
|
> /bye
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Pour les plus aventureux, la commande probablement la plus intéressante est <code class="language-plaintext highlighter-rouge">ADPU</code>, qui permet d’envoyer des commandes APDU brutes à la carte (rapportez-vous à la <a href="http://g10code.com/docs/openpgp-card-2.0.pdf">spécification de la carte OpenPGP</a> pour le détail des commandes APDU). Par exemple, pour lire les « octets historiques » de la carte :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg-connect-agent --hex 'SCD APDU 00CA5F5200' /bye
|
|||
|
D[0000] 00 31 C5 73 C0 01 40 05 90 00 90 00 .1.s..@.....
|
|||
|
OK
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<h3 id="ré-initialiser-une-carte-bloquée">Ré-initialiser une carte bloquée</h3>
|
|||
|
|
|||
|
<p>Comme expliqué dans la <a href="#s%C3%A9curit%C3%A9-de-la-carte">section « Sécurité »</a>, après trois saisies erronées consécutives du PIN utilisateur et du PIN administrateur, plus aucun PIN ne peut être vérifié et la carte est définitivement bloquée.</p>
|
|||
|
|
|||
|
<p>La version 2.0 de la carte OpenPGP prévoit toutefois la possibilité de ré-initialiser une carte bloquée. La ré-initialisation efface toutes les données de la carte et la ramène à son état de sortie d’usine.</p>
|
|||
|
|
|||
|
<p>Cette fonctionnalité est optionnelle. Pour savoir si une carte donnée est ré-initialisable, il faut consulter les « octets historiques » comme dans l’exemple à la fin de la section précédente :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg-connect-agent --hex 'SCD APDU 00CA5F5200' /bye
|
|||
|
D[0000] 00 31 C5 73 C0 01 40 05 90 00 90 00 .1.s..@.....
|
|||
|
OK
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Le huitième octet (<code class="language-plaintext highlighter-rouge">05</code>) est le « Life Cycle Status indicator ». Une valeur de 5 indique que la carte peut être ré-initialisée.</p>
|
|||
|
|
|||
|
<p>Pour ré-initialiser une telle carte, il faut lui envoyer successivement les deux commandes APDU « TERMINATE DF » et « ACTIVATE FILE », comme suit :</p>
|
|||
|
|
|||
|
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">$ </span>gpg-connect-agent
|
|||
|
<span class="o">></span> SCD APDU 00 E6 00 00
|
|||
|
<span class="o">></span> SCD APDU 00 44 00 00
|
|||
|
/bye
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Pour éviter toute erreur malheureuse, la commande « ACTIVATE FILE » ne fonctionne que si elle est utilisée après la commande « TERMINATE DF », et cette dernière n’a elle-même pas d’effet si la carte n’est pas bloquée.</p>
|
|||
|
|
|||
|
<p>Pour forcer la ré-initialisation de la carte quel que soit son état actuel (bloqué ou non), on peut utiliser <a href="http://lists.gnupg.org/pipermail/gnupg-users/2014-December/051951.html">le script suivant</a> (ou la commande <code class="language-plaintext highlighter-rouge">factory reset</code> de l’éditeur de carte de GnuPG, dans sa dernière version 2.1.1) :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> SCD RESET
|
|||
|
SCD SERIALNO undefined
|
|||
|
SCD APDU 00 A4 04 00 06 D2 76 00 01 24 01
|
|||
|
SCD APDU 00 20 00 81 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 81 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 81 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 81 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 83 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 83 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 83 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 20 00 83 08 40 40 40 40 40 40 40 40
|
|||
|
SCD APDU 00 e6 00 00
|
|||
|
SCD RESET
|
|||
|
SCD SERIALNO undefined
|
|||
|
SCD APDU 00 A4 04 00 06 D2 76 00 01 24 01
|
|||
|
SCD APDU 00 44 00 00
|
|||
|
/echo Card has been reset to factory defaults
|
|||
|
/bye
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Stockez ces commandes dans un fichier et envoyez le tout à la carte via l’agent GnuPG :</p>
|
|||
|
|
|||
|
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ gpg-connect-agent --hex --run fichier
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<blockquote>
|
|||
|
<p><strong>Attention</strong></p>
|
|||
|
|
|||
|
<p>Rappelez-vous que la ré-initialisation est une fonctionnalité optionnelle. N’exécutez surtout pas la commande précédente sans avoir vérifié que <em>votre</em> carte est ré-initialisable ! Si elle ne l’est pas, votre carte sera bel et bien <em>définitivement</em> bloquée.</p>
|
|||
|
</blockquote>
|
|||
|
|
|||
|
<div class="footnotes" role="doc-endnotes">
|
|||
|
<ol>
|
|||
|
<li id="fn:1" role="doc-endnote">
|
|||
|
<p>La carte n’est jamais utilisée pour <em>chiffrer</em>, puisqu’en cryptographie asymétrique, seul le déchiffrement nécessite une clef <em>privée</em> — le chiffrement utilise la clef publique du destinataire. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
|
|||
|
</li>
|
|||
|
<li id="fn:2" role="doc-endnote">
|
|||
|
<p>Commentaire en tête du fichier <code class="language-plaintext highlighter-rouge">scd/ccid-driver.c</code>, dans les sources de GnuPG. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">↩</a></p>
|
|||
|
</li>
|
|||
|
<li id="fn:3" role="doc-endnote">
|
|||
|
<p>À moins que vous ayez plus d’un certificat signé par l’autorité attendue par le serveur, auquel cas il vous demandera de sélectionner vous-même celui qu’il faut utiliser. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">↩</a></p>
|
|||
|
</li>
|
|||
|
</ol>
|
|||
|
</div>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
|
|||
|
<div class="d-print-none">
|
|||
|
<footer class="article__footer"><meta itemprop="dateModified" content="2019-12-25T00:00:00+01:00">
|
|||
|
<!-- start custom article footer snippet -->
|
|||
|
|
|||
|
<!-- end custom article footer snippet -->
|
|||
|
<!--
|
|||
|
<div align="right"><a type="application/rss+xml" href="/feed.xml" title="S'abonner"><i class="fa fa-rss fa-2x"></i></a>
|
|||
|
|
|||
|
 </div>
|
|||
|
-->
|
|||
|
</footer>
|
|||
|
<div class="article__section-navigator clearfix">
|
|||
|
<div class="previous">
|
|||
|
<span>PRÉCÉDENT</span><a href="/2019/12/25/nmap.html">nmap</a>
|
|||
|
</div>
|
|||
|
<div class="next">
|
|||
|
<span>SUIVANT</span><a href="/2019/12/25/openssh-debian-configuration.html">openssh-debian-configuration</a>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
$(function() {
|
|||
|
var $this ,$scroll;
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var scroll = hasSidebar ? '.js-page-main' : 'html, body';
|
|||
|
$scroll = $(scroll);
|
|||
|
|
|||
|
$articleContent.find('.highlight').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.attr('data-lang', $this.find('code').attr('data-lang'));
|
|||
|
});
|
|||
|
$articleContent.find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.append($('<a class="anchor d-print-none" aria-hidden="true"></a>').html('<i class="fas fa-anchor"></i>'));
|
|||
|
});
|
|||
|
$articleContent.on('click', '.anchor', function() {
|
|||
|
$scroll.scrollToAnchor('#' + $(this).parent().attr('id'), 400);
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
})();
|
|||
|
</script>
|
|||
|
|
|||
|
</div>
|
|||
|
<section class="page__comments d-print-none"></section></article><!-- start custom main bottom snippet -->
|
|||
|
<!-- end custom main bottom snippet -->
|
|||
|
</div>
|
|||
|
</div></div></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $body = $('body'), $window = $(window);
|
|||
|
var $pageRoot = $('.js-page-root'), $pageMain = $('.js-page-main');
|
|||
|
var activeCount = 0;
|
|||
|
function modal(options) {
|
|||
|
var $root = this, visible, onChange, hideWhenWindowScroll = false;
|
|||
|
var scrollTop;
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
visible = _options.initialVisible === undefined ? false : show;
|
|||
|
onChange = _options.onChange;
|
|||
|
hideWhenWindowScroll = _options.hideWhenWindowScroll;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
setState(visible);
|
|||
|
}
|
|||
|
function setState(isShow) {
|
|||
|
if (isShow === visible) {
|
|||
|
return;
|
|||
|
}
|
|||
|
visible = isShow;
|
|||
|
if (visible) {
|
|||
|
activeCount++;
|
|||
|
scrollTop = $(window).scrollTop() || $pageMain.scrollTop();
|
|||
|
$root.addClass('modal--show');
|
|||
|
$pageMain.scrollTop(scrollTop);
|
|||
|
activeCount === 1 && ($pageRoot.addClass('show-modal'), $body.addClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.on('scroll', hide);
|
|||
|
$window.on('keyup', handleKeyup);
|
|||
|
} else {
|
|||
|
activeCount > 0 && activeCount--;
|
|||
|
$root.removeClass('modal--show');
|
|||
|
$window.scrollTop(scrollTop);
|
|||
|
activeCount === 0 && ($pageRoot.removeClass('show-modal'), $body.removeClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.off('scroll', hide);
|
|||
|
$window.off('keyup', handleKeyup);
|
|||
|
}
|
|||
|
onChange && onChange(visible);
|
|||
|
}
|
|||
|
function show() {
|
|||
|
setState(true);
|
|||
|
}
|
|||
|
function hide() {
|
|||
|
setState(false);
|
|||
|
}
|
|||
|
function handleKeyup(e) {
|
|||
|
// Char Code: 27 ESC
|
|||
|
if (e.which === 27) {
|
|||
|
hide();
|
|||
|
}
|
|||
|
}
|
|||
|
setOptions(options);
|
|||
|
init();
|
|||
|
return {
|
|||
|
show: show,
|
|||
|
hide: hide,
|
|||
|
$el: $root
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.modal = modal;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="modal modal--overflow page__search-modal d-print-none js-page-search-modal">
|
|||
|
<script>
|
|||
|
(function () {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
// search panel
|
|||
|
var search = (window.search || (window.search = {}));
|
|||
|
var useDefaultSearchBox = window.useDefaultSearchBox === undefined ?
|
|||
|
true : window.useDefaultSearchBox ;
|
|||
|
|
|||
|
var $searchModal = $('.js-page-search-modal');
|
|||
|
var $searchToggle = $('.js-search-toggle');
|
|||
|
var searchModal = $searchModal.modal({ onChange: handleModalChange, hideWhenWindowScroll: true });
|
|||
|
var modalVisible = false;
|
|||
|
search.searchModal = searchModal;
|
|||
|
|
|||
|
var $searchBox = null;
|
|||
|
var $searchInput = null;
|
|||
|
var $searchClear = null;
|
|||
|
|
|||
|
function getModalVisible() {
|
|||
|
return modalVisible;
|
|||
|
}
|
|||
|
search.getModalVisible = getModalVisible;
|
|||
|
|
|||
|
function handleModalChange(visible) {
|
|||
|
modalVisible = visible;
|
|||
|
if (visible) {
|
|||
|
search.onShow && search.onShow();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].focus();
|
|||
|
} else {
|
|||
|
search.onShow && search.onHide();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].blur();
|
|||
|
setTimeout(function() {
|
|||
|
useDefaultSearchBox && ($searchInput.val(''), $searchBox.removeClass('not-empty'));
|
|||
|
search.clear && search.clear();
|
|||
|
window.pageAsideAffix && window.pageAsideAffix.refresh();
|
|||
|
}, 400);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$searchToggle.on('click', function() {
|
|||
|
modalVisible ? searchModal.hide() : searchModal.show();
|
|||
|
});
|
|||
|
// Char Code: 83 S, 191 /
|
|||
|
$(window).on('keyup', function(e) {
|
|||
|
if (!modalVisible && !window.isFormElement(e.target || e.srcElement) && (e.which === 83 || e.which === 191)) {
|
|||
|
modalVisible || searchModal.show();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (useDefaultSearchBox) {
|
|||
|
$searchBox = $('.js-search-box');
|
|||
|
$searchInput = $searchBox.children('input');
|
|||
|
$searchClear = $searchBox.children('.js-icon-clear');
|
|||
|
search.getSearchInput = function() {
|
|||
|
return $searchInput.get(0);
|
|||
|
};
|
|||
|
search.getVal = function() {
|
|||
|
return $searchInput.val();
|
|||
|
};
|
|||
|
search.setVal = function(val) {
|
|||
|
$searchInput.val(val);
|
|||
|
};
|
|||
|
|
|||
|
$searchInput.on('focus', function() {
|
|||
|
$(this).addClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('blur', function() {
|
|||
|
$(this).removeClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('input', window.throttle(function() {
|
|||
|
var val = $(this).val();
|
|||
|
if (val === '' || typeof val !== 'string') {
|
|||
|
search.clear && search.clear();
|
|||
|
} else {
|
|||
|
$searchBox.addClass('not-empty');
|
|||
|
search.onInputNotEmpty && search.onInputNotEmpty(val);
|
|||
|
}
|
|||
|
}, 400));
|
|||
|
$searchClear.on('click', function() {
|
|||
|
$searchInput.val(''); $searchBox.removeClass('not-empty');
|
|||
|
search.clear && search.clear();
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="search search--dark">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header">Recherche</div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text">
|
|||
|
<div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<button class="button button--theme-dark button--pill search__cancel js-search-toggle">
|
|||
|
Annuler</button>
|
|||
|
</div>
|
|||
|
<div id="results-container" class="search-result js-search-result"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
noResultsText: '<p>Aucun résultat!</p>',
|
|||
|
json: '/search.json',
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a> (Création {create})</li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function scrollToAnchor(anchor, duration, callback) {
|
|||
|
var $root = this;
|
|||
|
$root.animate({ scrollTop: $(anchor).position().top }, duration, function() {
|
|||
|
window.history.replaceState(null, '', window.location.href.split('#')[0] + anchor);
|
|||
|
callback && callback();
|
|||
|
});
|
|||
|
}
|
|||
|
$.fn.scrollToAnchor = scrollToAnchor;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function affix(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroll,
|
|||
|
offsetBottom = 0, scrollTarget = window, scroll = window.document, disabled = false, isOverallScroller = true,
|
|||
|
rootTop, rootLeft, rootHeight, scrollBottom, rootBottomTop,
|
|||
|
hasInit = false, curState;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.offsetBottom && (offsetBottom = _options.offsetBottom);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroll && (scroll = _options.scroll);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
isOverallScroller = window.isOverallScroller($scrollTarget[0]);
|
|||
|
$scroll = $(scroll);
|
|||
|
}
|
|||
|
function preCalc() {
|
|||
|
top();
|
|||
|
rootHeight = $root.outerHeight();
|
|||
|
rootTop = $root.offset().top + (isOverallScroller ? 0 : $scrollTarget.scrollTop());
|
|||
|
rootLeft = $root.offset().left;
|
|||
|
}
|
|||
|
function calc(needPreCalc) {
|
|||
|
needPreCalc && preCalc();
|
|||
|
scrollBottom = $scroll.outerHeight() - offsetBottom - rootHeight;
|
|||
|
rootBottomTop = scrollBottom - rootTop;
|
|||
|
}
|
|||
|
function top() {
|
|||
|
if (curState !== 'top') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'top';
|
|||
|
}
|
|||
|
}
|
|||
|
function fixed() {
|
|||
|
if (curState !== 'fixed') {
|
|||
|
$root.addClass('fixed').css({
|
|||
|
left: rootLeft + 'px',
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'fixed';
|
|||
|
}
|
|||
|
}
|
|||
|
function bottom() {
|
|||
|
if (curState !== 'bottom') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: rootBottomTop + 'px'
|
|||
|
});
|
|||
|
curState = 'bottom';
|
|||
|
}
|
|||
|
}
|
|||
|
function setState() {
|
|||
|
var scrollTop = $scrollTarget.scrollTop();
|
|||
|
if (scrollTop >= rootTop && scrollTop <= scrollBottom) {
|
|||
|
fixed();
|
|||
|
} else if (scrollTop < rootTop) {
|
|||
|
top();
|
|||
|
} else {
|
|||
|
bottom();
|
|||
|
}
|
|||
|
}
|
|||
|
function init() {
|
|||
|
if(!hasInit) {
|
|||
|
var interval, timeout;
|
|||
|
calc(true); setState();
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState();
|
|||
|
});
|
|||
|
$window.on('resize', function() {
|
|||
|
disabled || (calc(true), setState());
|
|||
|
});
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions,
|
|||
|
refresh: function() {
|
|||
|
calc(true, { animation: false }); setState();
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.affix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function toc(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroller, $tocUl = $('<ul class="toc toc--ellipsis"></ul>'), $tocLi, $headings, $activeLast, $activeCur,
|
|||
|
selectors = 'h1,h2,h3', container = 'body', scrollTarget = window, scroller = 'html, body', disabled = false,
|
|||
|
headingsPos, scrolling = false, hasRendered = false, hasInit = false;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.selectors && (selectors = _options.selectors);
|
|||
|
_options.container && (container = _options.container);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroller && (scroller = _options.scroller);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$headings = $(container).find(selectors).filter('[id]');
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
$scroller = $(scroller);
|
|||
|
}
|
|||
|
function calc() {
|
|||
|
headingsPos = [];
|
|||
|
$headings.each(function() {
|
|||
|
headingsPos.push(Math.floor($(this).position().top));
|
|||
|
});
|
|||
|
}
|
|||
|
function setState(element, disabled) {
|
|||
|
var scrollTop = $scrollTarget.scrollTop(), i;
|
|||
|
if (disabled || !headingsPos || headingsPos.length < 1) { return; }
|
|||
|
if (element) {
|
|||
|
$activeCur = element;
|
|||
|
} else {
|
|||
|
for (i = 0; i < headingsPos.length; i++) {
|
|||
|
if (scrollTop >= headingsPos[i]) {
|
|||
|
$activeCur = $tocLi.eq(i);
|
|||
|
} else {
|
|||
|
$activeCur || ($activeCur = $tocLi.eq(i));
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
$activeLast && $activeLast.removeClass('active');
|
|||
|
($activeLast = $activeCur).addClass('active');
|
|||
|
}
|
|||
|
function render() {
|
|||
|
if(!hasRendered) {
|
|||
|
$root.append($tocUl);
|
|||
|
$headings.each(function() {
|
|||
|
var $this = $(this);
|
|||
|
$tocUl.append($('<li></li>').addClass('toc-' + $this.prop('tagName').toLowerCase())
|
|||
|
.append($('<a></a>').text($this.text()).attr('href', '#' + $this.prop('id'))));
|
|||
|
});
|
|||
|
$tocLi = $tocUl.children('li');
|
|||
|
$tocUl.on('click', 'a', function(e) {
|
|||
|
e.preventDefault();
|
|||
|
var $this = $(this);
|
|||
|
scrolling = true;
|
|||
|
setState($this.parent());
|
|||
|
$scroller.scrollToAnchor($this.attr('href'), 400, function() {
|
|||
|
scrolling = false;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
hasRendered = true;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
var interval, timeout;
|
|||
|
if(!hasInit) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState(null, scrolling);
|
|||
|
});
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
if (!disabled) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
}
|
|||
|
}, 100));
|
|||
|
}
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.toc = toc;
|
|||
|
});
|
|||
|
})();
|
|||
|
/*(function () {
|
|||
|
|
|||
|
})();*/
|
|||
|
</script><script>
|
|||
|
/* toc must before affix, since affix need to konw toc' height. */(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
var TOC_SELECTOR = window.TEXT_VARIABLES.site.toc.selectors;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window);
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var $tocRoot = $('.js-toc-root'), $col2 = $('.js-col-aside');
|
|||
|
var toc;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var hasToc = $articleContent.find(TOC_SELECTOR).length > 0;
|
|||
|
|
|||
|
function disabled() {
|
|||
|
return $col2.css('display') === 'none' || !hasToc;
|
|||
|
}
|
|||
|
|
|||
|
tocDisabled = disabled();
|
|||
|
|
|||
|
toc = $tocRoot.toc({
|
|||
|
selectors: TOC_SELECTOR,
|
|||
|
container: $articleContent,
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
tocDisabled = disabled();
|
|||
|
toc && toc.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window), $pageFooter = $('.js-page-footer');
|
|||
|
var $pageAside = $('.js-page-aside');
|
|||
|
var affix;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
|
|||
|
affix = $pageAside.affix({
|
|||
|
offsetBottom: $pageFooter.outerHeight(),
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroll: hasSidebar ? $('.js-page-main').children() : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
affix && affix.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
window.pageAsideAffix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><!---->
|
|||
|
|
|||
|
</div>
|
|||
|
<script>(function () {
|
|||
|
var $root = document.getElementsByClassName('root')[0];
|
|||
|
if (window.hasEvent('touchstart')) {
|
|||
|
$root.dataset.isTouch = true;
|
|||
|
document.addEventListener('touchstart', function(){}, false);
|
|||
|
}
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|
|||
|
|