This repository has been archived by the owner on Feb 23, 2022. It is now read-only.
/
LogSignatureVerifier.java
365 lines (330 loc) · 16.6 KB
/
LogSignatureVerifier.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package org.certificatetransparency.ctlog;
import static org.certificatetransparency.ctlog.serialization.CTConstants.LOG_ENTRY_TYPE_LENGTH;
import static org.certificatetransparency.ctlog.serialization.CTConstants.MAX_CERTIFICATE_LENGTH;
import static org.certificatetransparency.ctlog.serialization.CTConstants.MAX_EXTENSIONS_LENGTH;
import static org.certificatetransparency.ctlog.serialization.CTConstants.TIMESTAMP_LENGTH;
import static org.certificatetransparency.ctlog.serialization.CTConstants.VERSION_LENGTH;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.TBSCertificate;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
import org.bouncycastle.util.encoders.Base64;
import org.certificatetransparency.ctlog.proto.Ct;
import org.certificatetransparency.ctlog.serialization.CTConstants;
import org.certificatetransparency.ctlog.serialization.Serializer;
import com.google.common.base.Preconditions;
/** Verifies signatures from a given CT Log. */
public class LogSignatureVerifier {
public static final String X509_AUTHORITY_KEY_IDENTIFIER = "2.5.29.35";
private final LogInfo logInfo;
/**
* Creates a new LogSignatureVerifier which is associated with a single log.
*
* @param logInfo information of the log this verifier is to be associated with.
*/
public LogSignatureVerifier(LogInfo logInfo) {
this.logInfo = logInfo;
}
private static class IssuerInformation {
private final X500Name name;
private final byte[] keyHash;
private final Extension x509authorityKeyIdentifier;
private final boolean issuedByPreCertificateSigningCert;
IssuerInformation(
X500Name name,
byte[] keyHash,
Extension x509authorityKeyIdentifier,
boolean issuedByPreCertificateSigningCert) {
this.name = name;
this.keyHash = keyHash;
this.x509authorityKeyIdentifier = x509authorityKeyIdentifier;
this.issuedByPreCertificateSigningCert = issuedByPreCertificateSigningCert;
}
X500Name getName() {
return name;
}
byte[] getKeyHash() {
return keyHash;
}
Extension getX509authorityKeyIdentifier() {
return x509authorityKeyIdentifier;
}
boolean issuedByPreCertificateSigningCert() {
return issuedByPreCertificateSigningCert;
}
}
static IssuerInformation issuerInformationFromPreCertificateSigningCert(
Certificate certificate, byte[] keyHash) {
try (ASN1InputStream aIssuerIn = new ASN1InputStream(certificate.getEncoded())) {
org.bouncycastle.asn1.x509.Certificate parsedIssuerCert =
org.bouncycastle.asn1.x509.Certificate.getInstance(aIssuerIn.readObject());
Extensions issuerExtensions = parsedIssuerCert.getTBSCertificate().getExtensions();
Extension x509authorityKeyIdentifier = null;
if (issuerExtensions != null) {
x509authorityKeyIdentifier =
issuerExtensions.getExtension(new ASN1ObjectIdentifier(X509_AUTHORITY_KEY_IDENTIFIER));
}
return new IssuerInformation(
parsedIssuerCert.getIssuer(), keyHash, x509authorityKeyIdentifier, true);
} catch (CertificateEncodingException e) {
throw new CertificateTransparencyException(
"Certificate could not be encoded: " + e.getMessage(), e);
} catch (IOException e) {
throw new CertificateTransparencyException(
"Error during ASN.1 parsing of certificate: " + e.getMessage(), e);
}
}
// Produces issuer information in case the PreCertificate is signed by a regular CA cert,
// not PreCertificate Signing Cert. In this case, the only thing that's needed is the
// issuer key hash - the Precertificate will already have the right value for the issuer
// name and K509 Authority Key Identifier extension.
static IssuerInformation issuerInformationFromCertificateIssuer(Certificate certificate) {
return new IssuerInformation(null, getKeyHash(certificate), null, false);
}
/**
* Verifies the CT Log's signature over the SCT and certificate. Works for the following cases:
*
* <ul>
* <li>Ordinary X509 certificate sent to the log.
* <li>PreCertificate signed by an ordinary CA certificate.
* <li>PreCertificate signed by a PreCertificate Signing Cert. In this case the PreCertificate
* signing certificate must be 2nd on the chain, the CA cert itself 3rd.
* </ul>
*
* @param sct SignedCertificateTimestamp received from the log.
* @param chain The certificates chain as sent to the log.
* @return true if the log's signature over this SCT can be verified, false otherwise.
*/
public boolean verifySignature(Ct.SignedCertificateTimestamp sct, List<Certificate> chain) {
if (sct != null && !logInfo.isSameLogId(sct.getId().getKeyId().toByteArray())) {
throw new CertificateTransparencyException(
String.format(
"Log ID of SCT (%s) does not match this log's ID (%s).",
Base64.toBase64String(sct.getId().getKeyId().toByteArray()),
Base64.toBase64String(logInfo.getID())));
}
X509Certificate leafCert = (X509Certificate) chain.get(0);
if (!CertificateInfo.isPreCertificate(leafCert) && !CertificateInfo.hasEmbeddedSCT(leafCert)) {
// When verifying final cert without embedded SCTs, we don't need the issuer but can verify directly
byte[] toVerify = serializeSignedSCTData(leafCert, sct);
return verifySCTSignatureOverBytes(sct, toVerify);
}
Preconditions.checkArgument(
chain.size() >= 2, "Chain with PreCertificate or Certificate must contain issuer.");
// PreCertificate or final certificate with embedded SCTs, we want the issuerInformation
Certificate issuerCert = chain.get(1);
IssuerInformation issuerInformation;
if (!CertificateInfo.isPreCertificateSigningCert(issuerCert)) {
// If signed by the real issuing CA
issuerInformation = issuerInformationFromCertificateIssuer(issuerCert);
} else {
Preconditions.checkArgument(
chain.size() >= 3,
"Chain with PreCertificate signed by PreCertificate Signing Cert must contain issuer.");
issuerInformation =
issuerInformationFromPreCertificateSigningCert(issuerCert, getKeyHash(chain.get(2)));
}
return verifySCTOverPreCertificate(sct, leafCert, issuerInformation);
}
/**
* Verifies the CT Log's signature over the SCT and leaf certificate.
*
* @param sct SignedCertificateTimestamp received from the log.
* @param leafCert leaf certificate sent to the log.
* @return true if the log's signature over this SCT can be verified, false otherwise.
*/
boolean verifySignature(Ct.SignedCertificateTimestamp sct, Certificate leafCert) {
if (!logInfo.isSameLogId(sct.getId().getKeyId().toByteArray())) {
throw new CertificateTransparencyException(
String.format(
"Log ID of SCT (%s) does not match this log's ID.", sct.getId().getKeyId()));
}
byte[] toVerify = serializeSignedSCTData(leafCert, sct);
return verifySCTSignatureOverBytes(sct, toVerify);
}
/**
* Verifies the CT Log's signature over the SCT and the PreCertificate, or a final certificate.
*
* @param sct SignedCertificateTimestamp received from the log.
* @param certificate the PreCertificate sent to the log for addition, or the final certificate
* with the embedded SCTs.
* @param issuerInfo Information on the issuer which will (or did) ultimately sign this
* PreCertificate. If the PreCertificate was signed using by a PreCertificate Signing Cert,
* the issuerInfo contains data on the final CA certificate used for signing.
* @return true if the SCT verifies, false otherwise.
*/
boolean verifySCTOverPreCertificate(
Ct.SignedCertificateTimestamp sct,
X509Certificate certificate,
IssuerInformation issuerInfo) {
Preconditions.checkNotNull(issuerInfo, "At the very least, the issuer key hash is needed.");
TBSCertificate preCertificateTBS = createTbsForVerification(certificate, issuerInfo);
try {
byte[] toVerify =
serializeSignedSCTDataForPreCertificate(
preCertificateTBS.getEncoded(), issuerInfo.getKeyHash(), sct);
return verifySCTSignatureOverBytes(sct, toVerify);
} catch (IOException e) {
throw new CertificateTransparencyException(
"TBSCertificate part could not be encoded: " + e.getMessage(), e);
}
}
private TBSCertificate createTbsForVerification(
X509Certificate preCertificate, IssuerInformation issuerInformation) {
Preconditions.checkArgument(preCertificate.getVersion() >= 3);
// We have to use bouncycastle's certificate parsing code because Java's X509 certificate
// parsing discards the order of the extensions. The signature from SCT we're verifying
// is over the TBSCertificate in its original form, including the order of the extensions.
// Get the list of extensions, in its original order, minus the poison extension.
try (ASN1InputStream aIn = new ASN1InputStream(preCertificate.getEncoded())) {
org.bouncycastle.asn1.x509.Certificate parsedPreCertificate =
org.bouncycastle.asn1.x509.Certificate.getInstance(aIn.readObject());
// Make sure that we have the X509akid of the real issuer if:
// The PreCertificate has this extension, AND:
// The PreCertificate was signed by a PreCertificate signing cert.
if (hasX509AuthorityKeyIdentifier(parsedPreCertificate)
&& issuerInformation.issuedByPreCertificateSigningCert()) {
Preconditions.checkArgument(issuerInformation.getX509authorityKeyIdentifier() != null);
}
List<Extension> orderedExtensions =
getExtensionsWithoutPoisonAndSCT(
parsedPreCertificate.getTBSCertificate().getExtensions(),
issuerInformation.getX509authorityKeyIdentifier());
V3TBSCertificateGenerator tbsCertificateGenerator = new V3TBSCertificateGenerator();
TBSCertificate tbsPart = parsedPreCertificate.getTBSCertificate();
// Copy certificate.
// Version 3 is implied by the generator.
tbsCertificateGenerator.setSerialNumber(tbsPart.getSerialNumber());
tbsCertificateGenerator.setSignature(tbsPart.getSignature());
if (issuerInformation.getName() != null) {
tbsCertificateGenerator.setIssuer(issuerInformation.getName());
} else {
tbsCertificateGenerator.setIssuer(tbsPart.getIssuer());
}
tbsCertificateGenerator.setStartDate(tbsPart.getStartDate());
tbsCertificateGenerator.setEndDate(tbsPart.getEndDate());
tbsCertificateGenerator.setSubject(tbsPart.getSubject());
tbsCertificateGenerator.setSubjectPublicKeyInfo(tbsPart.getSubjectPublicKeyInfo());
tbsCertificateGenerator.setIssuerUniqueID(tbsPart.getIssuerUniqueId());
tbsCertificateGenerator.setSubjectUniqueID(tbsPart.getSubjectUniqueId());
tbsCertificateGenerator.setExtensions(
new Extensions(orderedExtensions.toArray(new Extension[] {})));
return tbsCertificateGenerator.generateTBSCertificate();
} catch (CertificateException e) {
throw new CertificateTransparencyException("Certificate error: " + e.getMessage(), e);
} catch (IOException e) {
throw new CertificateTransparencyException("Error deleting extension: " + e.getMessage(), e);
}
}
private static boolean hasX509AuthorityKeyIdentifier(
org.bouncycastle.asn1.x509.Certificate cert) {
Extensions extensions = cert.getTBSCertificate().getExtensions();
return extensions.getExtension(new ASN1ObjectIdentifier(X509_AUTHORITY_KEY_IDENTIFIER)) != null;
}
private List<Extension> getExtensionsWithoutPoisonAndSCT(
Extensions extensions, Extension replacementX509authorityKeyIdentifier) {
ASN1ObjectIdentifier[] extensionsOidsArray = extensions.getExtensionOIDs();
Iterator<ASN1ObjectIdentifier> extensionsOids = Arrays.asList(extensionsOidsArray).iterator();
// Order is important, which is why a list is used.
ArrayList<Extension> outputExtensions = new ArrayList<Extension>();
while (extensionsOids.hasNext()) {
ASN1ObjectIdentifier extn = extensionsOids.next();
String extnId = extn.getId();
if (extnId.equals(CTConstants.POISON_EXTENSION_OID)) {
// Do nothing - skip copying this extension
} else if (extnId.equals(CTConstants.SCT_CERTIFICATE_OID)) {
// Do nothing - skip copying this extension
} else if ((extnId.equals(X509_AUTHORITY_KEY_IDENTIFIER))
&& (replacementX509authorityKeyIdentifier != null)) {
// Use the real issuer's authority key identifier, since it's present.
outputExtensions.add(replacementX509authorityKeyIdentifier);
} else {
// Copy the extension as-is.
outputExtensions.add(extensions.getExtension(extn));
}
}
return outputExtensions;
}
private boolean verifySCTSignatureOverBytes(Ct.SignedCertificateTimestamp sct, byte[] toVerify) {
final String sigAlg;
if (logInfo.getSignatureAlgorithm().equals("EC")) {
sigAlg = "SHA256withECDSA";
} else if (logInfo.getSignatureAlgorithm().equals("RSA")) {
sigAlg = "SHA256withRSA";
} else {
throw new CertificateTransparencyException(
String.format("Unsupported signature algorithm %s", logInfo.getSignatureAlgorithm()));
}
try {
Signature signature = Signature.getInstance(sigAlg);
signature.initVerify(logInfo.getKey());
signature.update(toVerify);
return signature.verify(sct.getSignature().getSignature().toByteArray());
} catch (SignatureException e) {
throw new CertificateTransparencyException(
"Signature object not properly initialized or"
+ " signature from SCT is improperly encoded.",
e);
} catch (InvalidKeyException e) {
throw new CertificateTransparencyException("Log's public key cannot be used", e);
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedCryptoPrimitiveException(sigAlg + " not supported by this JVM", e);
}
}
static byte[] serializeSignedSCTData(Certificate certificate, Ct.SignedCertificateTimestamp sct) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
serializeCommonSCTFields(sct, bos);
Serializer.writeUint(bos, Ct.LogEntryType.X509_ENTRY_VALUE, LOG_ENTRY_TYPE_LENGTH);
try {
Serializer.writeVariableLength(bos, certificate.getEncoded(), MAX_CERTIFICATE_LENGTH);
} catch (CertificateEncodingException e) {
throw new CertificateTransparencyException("Error encoding certificate", e);
}
Serializer.writeVariableLength(bos, sct.getExtensions().toByteArray(), MAX_EXTENSIONS_LENGTH);
return bos.toByteArray();
}
static byte[] serializeSignedSCTDataForPreCertificate(
byte[] preCertBytes, byte[] issuerKeyHash, Ct.SignedCertificateTimestamp sct) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
serializeCommonSCTFields(sct, bos);
Serializer.writeUint(bos, Ct.LogEntryType.PRECERT_ENTRY_VALUE, LOG_ENTRY_TYPE_LENGTH);
Serializer.writeFixedBytes(bos, issuerKeyHash);
Serializer.writeVariableLength(bos, preCertBytes, MAX_CERTIFICATE_LENGTH);
Serializer.writeVariableLength(bos, sct.getExtensions().toByteArray(), MAX_EXTENSIONS_LENGTH);
return bos.toByteArray();
}
private static byte[] getKeyHash(Certificate signerCert) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(signerCert.getPublicKey().getEncoded());
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedCryptoPrimitiveException("SHA-256 not supported: " + e.getMessage(), e);
}
}
private static void serializeCommonSCTFields(
Ct.SignedCertificateTimestamp sct, ByteArrayOutputStream bos) {
Preconditions.checkArgument(
sct.getVersion().equals(Ct.Version.V1), "Can only serialize SCT v1 for now.");
Serializer.writeUint(bos, sct.getVersion().getNumber(), VERSION_LENGTH); // ct::V1
Serializer.writeUint(bos, 0, 1); // ct::CERTIFICATE_TIMESTAMP
Serializer.writeUint(bos, sct.getTimestamp(), TIMESTAMP_LENGTH); // Timestamp
}
}