1 /*
2 * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 or (at your option)
7 * version 3 of the License.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "Signature.h"
19 #include "core/Tools.h"
20 #include "crypto/Crypto.h"
21 #include "crypto/CryptoHash.h"
22 #include "crypto/ssh/OpenSSHKey.h"
23
24 #include <QByteArray>
25 #include <gcrypt.h>
26
27 struct RSASigner
28 {
29 gcry_error_t rc;
30 QString error;
31
raiseErrorRSASigner32 void raiseError(const QString& message = QString())
33 {
34 if (message.isEmpty()) {
35 error = QString("%1/%2").arg(QString::fromLocal8Bit(gcry_strsource(rc)),
36 QString::fromLocal8Bit(gcry_strerror(rc)));
37 } else {
38 error = message;
39 }
40 }
41
RSASignerRSASigner42 RSASigner()
43 : rc(GPG_ERR_NO_ERROR)
44 {
45 }
46
signRSASigner47 QString sign(const QByteArray& data, const OpenSSHKey& key)
48 {
49 enum Index
50 {
51 N,
52 E,
53 D,
54 P,
55 Q,
56 U, // private key
57 R,
58 S, // signature
59
60 Data,
61 Key,
62 Sig
63 };
64
65 const QList<QByteArray> parts = key.privateParts();
66 if (parts.count() != 6) {
67 raiseError("Unsupported signing key");
68 return QString();
69 }
70
71 const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256);
72
73 Tools::Map<Index, gcry_mpi_t, &gcry_mpi_release> mpi;
74 Tools::Map<Index, gcry_sexp_t, &gcry_sexp_release> sexp;
75 const gcry_mpi_format format = GCRYMPI_FMT_USG;
76 rc = gcry_mpi_scan(&mpi[N], format, parts[0].data(), parts[0].size(), nullptr);
77 if (rc != GPG_ERR_NO_ERROR) {
78 raiseError();
79 return QString();
80 }
81 rc = gcry_mpi_scan(&mpi[E], format, parts[1].data(), parts[1].size(), nullptr);
82 if (rc != GPG_ERR_NO_ERROR) {
83 raiseError();
84 return QString();
85 }
86 rc = gcry_mpi_scan(&mpi[D], format, parts[2].data(), parts[2].size(), nullptr);
87 if (rc != GPG_ERR_NO_ERROR) {
88 raiseError();
89 return QString();
90 }
91 rc = gcry_mpi_scan(&mpi[U], format, parts[3].data(), parts[3].size(), nullptr);
92 if (rc != GPG_ERR_NO_ERROR) {
93 raiseError();
94 return QString();
95 }
96 rc = gcry_mpi_scan(&mpi[P], format, parts[4].data(), parts[4].size(), nullptr);
97 if (rc != GPG_ERR_NO_ERROR) {
98 raiseError();
99 return QString();
100 }
101 rc = gcry_mpi_scan(&mpi[Q], format, parts[5].data(), parts[5].size(), nullptr);
102 if (rc != GPG_ERR_NO_ERROR) {
103 raiseError();
104 return QString();
105 }
106 if (gcry_mpi_cmp(mpi[P], mpi[Q]) > 0) {
107 // see https://www.gnupg.org/documentation/manuals/gcrypt/RSA-key-parameters.html#RSA-key-parameters
108 gcry_mpi_swap(mpi[P], mpi[Q]);
109 gcry_mpi_invm(mpi[U], mpi[P], mpi[Q]);
110 }
111 rc = gcry_sexp_build(&sexp[Key],
112 NULL,
113 "(private-key (rsa (n %m) (e %m) (d %m) (p %m) (q %m) (u %m)))",
114 mpi[N],
115 mpi[E],
116 mpi[D],
117 mpi[P],
118 mpi[Q],
119 mpi[U]);
120 if (rc != GPG_ERR_NO_ERROR) {
121 raiseError();
122 return QString();
123 }
124
125 rc = gcry_pk_testkey(sexp[Key]);
126 if (rc != GPG_ERR_NO_ERROR) {
127 raiseError();
128 return QString();
129 }
130
131 rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data());
132 // rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data());
133 if (rc != GPG_ERR_NO_ERROR) {
134 raiseError();
135 return QString();
136 }
137 rc = gcry_pk_sign(&sexp[Sig], sexp[Data], sexp[Key]);
138 if (rc != GPG_ERR_NO_ERROR) {
139 raiseError();
140 return QString();
141 }
142 sexp[S] = gcry_sexp_find_token(sexp[Sig], "s", 1);
143 mpi[S] = gcry_sexp_nth_mpi(sexp[S], 1, GCRYMPI_FMT_USG);
144 Tools::Buffer buffer;
145 rc = gcry_mpi_aprint(GCRYMPI_FMT_STD, &buffer.raw, &buffer.size, mpi[S]);
146 if (rc != GPG_ERR_NO_ERROR) {
147 raiseError();
148 return QString();
149 }
150 return QString("rsa|%1").arg(QString::fromLatin1(buffer.content().toHex()));
151 }
152
verifyRSASigner153 bool verify(const QByteArray& data, const OpenSSHKey& key, const QString& signature)
154 {
155 const gcry_mpi_format format = GCRYMPI_FMT_USG;
156 enum MPI
157 {
158 N,
159 E, // public key
160 R,
161 S // signature
162 };
163 enum SEXP
164 {
165 Data,
166 Key,
167 Sig
168 };
169
170 const QList<QByteArray> parts = key.publicParts();
171 if (parts.count() != 2) {
172 raiseError("Unsupported verification key");
173 return false;
174 }
175
176 const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256);
177
178 Tools::Map<MPI, gcry_mpi_t, &gcry_mpi_release> mpi;
179 Tools::Map<SEXP, gcry_sexp_t, &gcry_sexp_release> sexp;
180
181 rc = gcry_mpi_scan(&mpi[E], format, parts[0].data(), parts[0].size(), nullptr);
182 if (rc != GPG_ERR_NO_ERROR) {
183 raiseError();
184 return false;
185 }
186 rc = gcry_mpi_scan(&mpi[N], format, parts[1].data(), parts[1].size(), nullptr);
187 if (rc != GPG_ERR_NO_ERROR) {
188 raiseError();
189 return false;
190 }
191 rc = gcry_sexp_build(&sexp[Key], NULL, "(public-key (rsa (n %m) (e %m)))", mpi[N], mpi[E]);
192 if (rc != GPG_ERR_NO_ERROR) {
193 raiseError();
194 return false;
195 }
196
197 QRegExp extractor("rsa\\|([a-f0-9]+)", Qt::CaseInsensitive);
198 if (!extractor.exactMatch(signature) || extractor.captureCount() != 1) {
199 raiseError("Could not unpack signature parts");
200 return false;
201 }
202 const QByteArray sig_s = QByteArray::fromHex(extractor.cap(1).toLatin1());
203
204 rc = gcry_mpi_scan(&mpi[S], GCRYMPI_FMT_STD, sig_s.data(), sig_s.size(), nullptr);
205 if (rc != GPG_ERR_NO_ERROR) {
206 raiseError();
207 return false;
208 }
209 rc = gcry_sexp_build(&sexp[Sig], NULL, "(sig-val (rsa (s %m)))", mpi[S]);
210 if (rc != GPG_ERR_NO_ERROR) {
211 raiseError();
212 return false;
213 }
214 rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data());
215 // rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data());
216 if (rc != GPG_ERR_NO_ERROR) {
217 raiseError();
218 return false;
219 }
220 rc = gcry_pk_verify(sexp[Sig], sexp[Data], sexp[Key]);
221 if (rc != GPG_ERR_NO_ERROR && rc != GPG_ERR_BAD_SIGNATURE) {
222 raiseError();
223 return false;
224 }
225 return rc != GPG_ERR_BAD_SIGNATURE;
226 }
227 };
228
create(const QByteArray & data,const OpenSSHKey & key)229 QString Signature::create(const QByteArray& data, const OpenSSHKey& key)
230 {
231 // TODO HNH: currently we publish the signature in our own non-standard format - it would
232 // be better to use a standard format (like ASN1 - but this would be more easy
233 // when we integrate a proper library)
234 // Even more, we could publish standard self signed certificates with the container
235 // instead of the custom certificates
236 if (key.type() == "ssh-rsa") {
237 RSASigner signer;
238 QString result = signer.sign(data, key);
239 if (signer.rc != GPG_ERR_NO_ERROR) {
240 ::qWarning() << signer.error;
241 }
242 return result;
243 }
244 ::qWarning() << "Unsupported Public/Private key format";
245 return QString();
246 }
247
verify(const QByteArray & data,const QString & signature,const OpenSSHKey & key)248 bool Signature::verify(const QByteArray& data, const QString& signature, const OpenSSHKey& key)
249 {
250 if (key.type() == "ssh-rsa") {
251 RSASigner signer;
252 bool result = signer.verify(data, key, signature);
253 if (signer.rc != GPG_ERR_NO_ERROR) {
254 ::qWarning() << signer.error;
255 }
256 return result;
257 }
258 ::qWarning() << "Unsupported Public/Private key format";
259 return false;
260 }
261