Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(34)

Side by Side Diff: net/cert/internal/parse_ocsp.cc

Issue 1541213002: Adding OCSP Parser (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Moving Verify to end. Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <algorithm>
6
7 #include "base/sha1.h"
8 #include "crypto/sha2.h"
9 #include "net/cert/internal/extended_key_usage.h"
10 #include "net/cert/internal/parse_ocsp.h"
11 #include "net/cert/internal/signature_policy.h"
12 #include "net/cert/internal/verify_name_match.h"
13 #include "net/cert/internal/verify_signed_data.h"
14
15 namespace net {
16
17 OCSPCertID::OCSPCertID() {}
18 OCSPCertID::~OCSPCertID() {}
19
20 OCSPSingleResponse::OCSPSingleResponse() {}
21 OCSPSingleResponse::~OCSPSingleResponse() {}
22
23 OCSPResponseData::OCSPResponseData() {}
24 OCSPResponseData::~OCSPResponseData() {}
25
26 OCSPResponse::OCSPResponse() {}
27 OCSPResponse::~OCSPResponse() {}
28
29 der::Input BasicOCSPResponseOid() {
30 // From RFC 6960:
31 //
32 // id-pkix-ocsp OBJECT IDENTIFIER ::= { id-ad-ocsp }
33 // id-pkix-ocsp-basic OBJECT IDENTIFIER ::= { id-pkix-ocsp 1 }
34 //
35 // In dotted notation: 1.3.6.1.5.5.7.48.1.1
36 static const uint8_t oid[] = {0x2b, 0x06, 0x01, 0x05, 0x05,
37 0x07, 0x30, 0x01, 0x01};
38 return der::Input(oid);
39 }
40
41 // CertID ::= SEQUENCE {
42 // hashAlgorithm AlgorithmIdentifier,
43 // issuerNameHash OCTET STRING, -- Hash of issuer's DN
44 // issuerKeyHash OCTET STRING, -- Hash of issuer's public key
45 // serialNumber CertificateSerialNumber
46 // }
47 bool ParseOCSPCertID(const der::Input& raw_tlv, OCSPCertID* out) {
48 der::Parser outer_parser(raw_tlv);
49 der::Parser parser;
50 if (!outer_parser.ReadSequence(&parser))
51 return false;
52 if (outer_parser.HasMore())
53 return false;
54
55 der::Input sigalg_tlv;
56 if (!parser.ReadRawTLV(&sigalg_tlv))
57 return false;
58 if (!ParseHashAlgorithm(sigalg_tlv, &(out->hash_algorithm)))
59 return false;
60 if (!parser.ReadTag(der::kOctetString, &(out->issuer_name_hash)))
61 return false;
62 if (!parser.ReadTag(der::kOctetString, &(out->issuer_key_hash)))
63 return false;
64 if (!parser.ReadTag(der::kInteger, &(out->serial_number)))
65 return false;
66
67 bool unused_negative;
68 if (!der::IsValidInteger(out->serial_number, &unused_negative))
69 return false;
70 if (out->serial_number.Length() == 21 &&
eroman 2016/02/16 23:42:26 Any plan to re-use VerifySerialNumber? Also any de
svaldez 2016/02/17 16:46:47 Fixing to use upstream CL changes.
71 out->serial_number.UnsafeData()[0] == 0) {
72 return true;
73 }
74 if (out->serial_number.Length() > 20)
75 return false;
76
77 return !parser.HasMore();
78 }
79
80 namespace {
eroman 2016/02/16 23:42:25 nit: Unnamed namespaces are typically put at the s
svaldez 2016/02/17 16:46:46 I think its slightly more straightforward to keep
81
82 // Parses |raw_tlv| to extract an OCSP RevokedInfo (RFC 6960) and stores the
83 // result in the OCSPCertStatus |out|. Returns whether the parsing was
84 // successful.
85 //
86 // RevokedInfo ::= SEQUENCE {
87 // revocationTime GeneralizedTime,
88 // revocationReason [0] EXPLICIT CRLReason OPTIONAL
89 // }
90 bool ParseRevokedInfo(const der::Input& raw_tlv, OCSPCertStatus* out) {
91 der::Parser parser(raw_tlv);
92 if (!parser.ReadGeneralizedTime(&(out->revocation_time)))
93 return false;
94
95 der::Input reason_input;
96 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(0), &reason_input,
97 &(out->has_reason))) {
98 return false;
99 }
100 if (out->has_reason) {
101 der::Parser reason_parser(reason_input);
102 der::Input reason_value_input;
103 uint8_t reason_value;
104 if (!reason_parser.ReadTag(der::kEnumerated, &reason_value_input))
105 return false;
106 if (!der::ParseUint8(reason_value_input, &reason_value))
107 return false;
108 if (reason_value >
109 static_cast<uint8_t>(OCSPCertStatus::RevocationReason::LAST)) {
110 return false;
111 }
112 out->revocation_reason =
113 static_cast<OCSPCertStatus::RevocationReason>(reason_value);
114 if (out->revocation_reason == OCSPCertStatus::RevocationReason::UNUSED)
115 return false;
116 if (reason_parser.HasMore())
117 return false;
118 }
119 return !parser.HasMore();
120 }
121
122 // Parses |raw_tlv| to extract an OCSP CertStatus (RFC 6960) and stores the
123 // result in the OCSPCertStatus |out|. Returns whether the parsing was
124 // successful.
125 //
126 // CertStatus ::= CHOICE {
127 // good [0] IMPLICIT NULL,
128 // revoked [1] IMPLICIT RevokedInfo,
129 // unknown [2] IMPLICIT UnknownInfo
130 // }
131 //
132 // UnknownInfo ::= NULL
133 bool ParseCertStatus(const der::Input& raw_tlv, OCSPCertStatus* out) {
eroman 2016/02/16 23:42:26 Thanks, this is easier to follow now IMO
svaldez 2016/02/17 16:46:46 Acknowledged.
134 der::Parser parser(raw_tlv);
135 der::Tag status_tag;
136 der::Input status;
137 if (!parser.ReadTagAndValue(&status_tag, &status))
138 return false;
139
140 out->has_reason = false;
141 if (status_tag == der::ContextSpecificPrimitive(0)) {
142 out->status = OCSPCertStatus::Status::GOOD;
143 } else if (status_tag == der::ContextSpecificConstructed(1)) {
144 out->status = OCSPCertStatus::Status::REVOKED;
145 if (!ParseRevokedInfo(status, out))
146 return false;
147 } else if (status_tag == der::ContextSpecificPrimitive(2)) {
148 out->status = OCSPCertStatus::Status::UNKNOWN;
149 } else {
150 return false;
151 }
152
153 return !parser.HasMore();
154 }
155
156 } // namespace
157
158 // SingleResponse ::= SEQUENCE {
159 // certID CertID,
160 // certStatus CertStatus,
161 // thisUpdate GeneralizedTime,
162 // nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL,
163 // singleExtensions [1] EXPLICIT Extensions OPTIONAL
164 // }
165 bool ParseOCSPSingleResponse(const der::Input& raw_tlv,
166 OCSPSingleResponse* out) {
167 der::Parser outer_parser(raw_tlv);
168 der::Parser parser;
169 if (!outer_parser.ReadSequence(&parser))
170 return false;
171 if (outer_parser.HasMore())
172 return false;
173
174 if (!parser.ReadRawTLV(&(out->cert_id_tlv)))
175 return false;
176 der::Input status_tlv;
177 if (!parser.ReadRawTLV(&status_tlv))
178 return false;
179 if (!ParseCertStatus(status_tlv, &(out->cert_status)))
180 return false;
181 if (!parser.ReadGeneralizedTime(&(out->this_update)))
182 return false;
183
184 der::Input next_update_input;
185 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(0),
186 &next_update_input, &(out->has_next_update))) {
187 return false;
188 }
189 if (out->has_next_update) {
190 der::Parser next_update_parser(next_update_input);
191 if (!next_update_parser.ReadGeneralizedTime(&(out->next_update)))
192 return false;
193 if (next_update_parser.HasMore())
194 return false;
195 }
196
197 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(1),
198 &(out->extensions), &(out->has_extensions))) {
199 return false;
200 }
201
202 return !parser.HasMore();
203 }
204
205 namespace {
eroman 2016/02/16 23:42:26 [optional] Same comment as previously. I think it
svaldez 2016/02/17 16:46:46 See above.
206
207 // Parses |raw_tlv| to extract a ResponderID (RFC 6960) and stores the
208 // result in the ResponderID |out|. Returns whether the parsing was successful.
209 //
210 // ResponderID ::= CHOICE {
211 // byName [1] Name,
212 // byKey [2] KeyHash
213 // }
214 bool ParseResponderID(const der::Input& raw_tlv,
215 OCSPResponseData::ResponderID* out) {
216 der::Parser parser(raw_tlv);
217 der::Tag id_tag;
218 der::Input id_input;
219 if (!parser.ReadTagAndValue(&id_tag, &id_input))
220 return false;
221
222 if (id_tag == der::ContextSpecificConstructed(1)) {
223 out->type = OCSPResponseData::ResponderType::NAME;
224 out->name = id_input;
225 } else if (id_tag == der::ContextSpecificConstructed(2)) {
226 der::Parser key_parser(id_input);
227 der::Input responder_key;
228 if (!key_parser.ReadTag(der::kOctetString, &responder_key))
229 return false;
230 if (key_parser.HasMore())
231 return false;
232
233 SHA1HashValue key_hash;
234 if (responder_key.Length() != sizeof(key_hash.data))
235 return false;
236 memcpy(key_hash.data, responder_key.UnsafeData(), sizeof(key_hash.data));
237 out->type = OCSPResponseData::ResponderType::KEY_HASH;
238 out->key_hash = HashValue(key_hash);
239 } else {
240 return false;
241 }
242 return !parser.HasMore();
243 }
244
245 } // namespace
246
247 // ResponseData ::= SEQUENCE {
248 // version [0] EXPLICIT Version DEFAULT v1,
249 // responderID ResponderID,
250 // producedAt GeneralizedTime,
251 // responses SEQUENCE OF SingleResponse,
252 // responseExtensions [1] EXPLICIT Extensions OPTIONAL
253 // }
254 bool ParseOCSPResponseData(const der::Input& raw_tlv, OCSPResponseData* out) {
255 der::Parser outer_parser(raw_tlv);
256 der::Parser parser;
257 if (!outer_parser.ReadSequence(&parser))
258 return false;
259 if (outer_parser.HasMore())
260 return false;
261
262 der::Input version_input;
263 bool version_present;
264 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(0),
265 &version_input, &version_present)) {
266 return false;
267 }
268
269 // For compatibilty, the restriction that DEFAULT values should be omitted is
270 // ignored since many implementations include them.
eroman 2016/02/16 23:42:26 Can you include a TODO or some other way of findin
svaldez 2016/02/17 16:46:46 I no longer believe this restriction to be true, f
svaldez 2016/02/17 18:12:20 Followed by more reading of the ITU specs which le
271 if (version_present) {
272 der::Parser version_parser(version_input);
273 if (!version_parser.ReadUint64(&(out->version)))
274 return false;
275 if (version_parser.HasMore())
276 return false;
277 } else {
278 out->version = 0;
279 }
280
281 der::Input responder_input;
282 if (!parser.ReadRawTLV(&responder_input))
283 return false;
284 if (!ParseResponderID(responder_input, &(out->responder_id)))
285 return false;
286 if (!parser.ReadGeneralizedTime(&(out->produced_at)))
287 return false;
288
289 der::Parser responses_parser;
290 if (!parser.ReadSequence(&responses_parser))
291 return false;
292 out->responses.clear();
293 while (responses_parser.HasMore()) {
294 der::Input single_response;
295 if (!responses_parser.ReadRawTLV(&single_response))
296 return false;
297 out->responses.push_back(single_response);
298 }
299
300 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(1),
301 &(out->extensions), &(out->has_extensions))) {
302 return false;
303 }
304
305 return !parser.HasMore();
306 }
307
308 namespace {
309
310 // Parses |raw_tlv| to extract a BasicOCSPResponse (RFC 6960) and stores the
311 // result in the OCSPResponse |out|. Returns whether the parsing was
312 // successful.
313 //
314 // BasicOCSPResponse ::= SEQUENCE {
315 // tbsResponseData ResponseData,
316 // signatureAlgorithm AlgorithmIdentifier,
317 // signature BIT STRING,
318 // certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL
319 // }
320 bool ParseBasicOCSPResponse(const der::Input& raw_tlv, OCSPResponse* out) {
321 der::Parser outer_parser(raw_tlv);
322 der::Parser parser;
323 if (!outer_parser.ReadSequence(&parser))
324 return false;
325 if (outer_parser.HasMore())
326 return false;
327
328 if (!parser.ReadRawTLV(&(out->data)))
329 return false;
330 der::Input sigalg_tlv;
331 if (!parser.ReadRawTLV(&sigalg_tlv))
332 return false;
333 out->signature_algorithm = SignatureAlgorithm::CreateFromDer(sigalg_tlv);
334 if (!out->signature_algorithm)
335 return false;
336 if (!parser.ReadBitString(&(out->signature)))
337 return false;
338 der::Input certs_input;
339 if (!parser.ReadOptionalTag(der::ContextSpecificConstructed(0), &certs_input,
340 &(out->has_certs))) {
341 return false;
342 }
343
344 if (out->has_certs) {
345 der::Parser certs_seq_parser(certs_input);
346 der::Parser certs_parser;
347 if (!certs_seq_parser.ReadSequence(&certs_parser))
348 return false;
349 if (certs_seq_parser.HasMore())
350 return false;
351 out->certs.clear();
eroman 2016/02/16 23:42:26 I suggest doing this unconditionally outside of "i
svaldez 2016/02/17 16:46:46 Done.
352 while (certs_parser.HasMore()) {
353 der::Input cert_tlv;
354 if (!certs_parser.ReadRawTLV(&cert_tlv))
355 return false;
356 out->certs.push_back(cert_tlv);
357 }
358 }
359
360 return !parser.HasMore();
361 }
362
363 } // namespace
364
365 // OCSPResponse ::= SEQUENCE {
366 // responseStatus OCSPResponseStatus,
367 // responseBytes [0] EXPLICIT ResponseBytes OPTIONAL
368 // }
369 //
370 // ResponseBytes ::= SEQUENCE {
371 // responseType OBJECT IDENTIFIER,
372 // response OCTET STRING
373 // }
374 bool ParseOCSPResponse(const der::Input& raw_tlv, OCSPResponse* out) {
375 der::Parser outer_parser(raw_tlv);
376 der::Parser parser;
377 if (!outer_parser.ReadSequence(&parser))
378 return false;
379 if (outer_parser.HasMore())
380 return false;
381
382 der::Input response_status_input;
383 uint8_t response_status;
384 if (!parser.ReadTag(der::kEnumerated, &response_status_input))
385 return false;
386 if (!der::ParseUint8(response_status_input, &response_status))
387 return false;
388 if (response_status >
389 static_cast<uint8_t>(OCSPResponse::ResponseStatus::LAST)) {
390 return false;
391 }
392 out->status = static_cast<OCSPResponse::ResponseStatus>(response_status);
393 if (out->status == OCSPResponse::ResponseStatus::UNUSED)
394 return false;
395
396 if (out->status == OCSPResponse::ResponseStatus::SUCCESSFUL) {
397 der::Parser outer_bytes_parser;
398 der::Parser bytes_parser;
399 if (!parser.ReadConstructed(der::ContextSpecificConstructed(0),
400 &outer_bytes_parser)) {
401 return false;
402 }
403 if (!outer_bytes_parser.ReadSequence(&bytes_parser))
404 return false;
405 if (outer_bytes_parser.HasMore())
406 return false;
407
408 der::Input type_oid;
409 if (!bytes_parser.ReadTag(der::kOid, &type_oid))
410 return false;
411 if (type_oid != BasicOCSPResponseOid())
412 return false;
413
414 // As per RFC 6960 Section 4.2.1, the value of |response| SHALL be the DER
415 // encoding of BasicOCSPResponse.
416 der::Input response;
417 if (!bytes_parser.ReadTag(der::kOctetString, &response))
418 return false;
419 if (!ParseBasicOCSPResponse(response, out))
420 return false;
421 if (bytes_parser.HasMore())
422 return false;
423 }
424
425 return !parser.HasMore();
426 }
427
428 namespace {
429
430 // Checks that the |type| hash of |value| is equal to |hash|
431 bool VerifyHash(HashValueTag type,
eroman 2016/02/16 23:42:25 Are you going to remove the other stuff from this
svaldez 2016/02/17 16:46:47 I think it probably makes more sense to keep these
432 const der::Input& hash,
433 const der::Input& value) {
434 HashValue target(type);
435 if (target.size() != hash.Length())
436 return false;
437 memcpy(target.data(), hash.UnsafeData(), target.size());
438
439 HashValue value_hash(type);
440 if (type == HASH_VALUE_SHA1) {
441 base::SHA1HashBytes(value.UnsafeData(), value.Length(), value_hash.data());
442 } else if (type == HASH_VALUE_SHA256) {
443 std::string hash_string = crypto::SHA256HashString(value.AsString());
444 memcpy(value_hash.data(), hash_string.data(), value_hash.size());
445 } else {
446 return false;
447 }
448
449 return target.Equals(value_hash);
450 }
451
452 // Checks that the input |id_tlv| parses to a valid CertID and matches the
453 // issuer |issuer| and serial number |serial_number|.
454 bool CheckCertID(const der::Input& id_tlv,
455 const ParsedTbsCertificate& issuer,
456 const der::Input& serial_number) {
457 OCSPCertID id;
458 if (!ParseOCSPCertID(id_tlv, &id))
459 return false;
460
461 HashValueTag type;
462 switch (id.hash_algorithm) {
463 case DigestAlgorithm::Sha1:
464 type = HASH_VALUE_SHA1;
465 break;
466 case DigestAlgorithm::Sha256:
467 type = HASH_VALUE_SHA256;
468 break;
469 default:
eroman 2016/02/16 23:42:25 Please remove the default case, and instead explic
svaldez 2016/02/17 16:46:46 Done.
470 NOTIMPLEMENTED();
471 return false;
472 }
473
474 if (!VerifyHash(type, id.issuer_name_hash, issuer.subject_tlv))
475 return false;
476
477 der::Parser outer_parser(issuer.spki_tlv);
478 der::Parser spki_parser;
479 der::BitString key_bits;
480 if (!outer_parser.ReadSequence(&spki_parser))
481 return false;
482 if (outer_parser.HasMore())
483 return false;
484 if (!spki_parser.SkipTag(der::kSequence))
485 return false;
486 if (!spki_parser.ReadBitString(&key_bits))
487 return false;
488 der::Input key_tlv = key_bits.bytes();
489 if (!VerifyHash(type, id.issuer_key_hash, key_tlv))
490 return false;
491
492 return id.serial_number == serial_number;
eroman 2016/02/16 23:42:26 Why this particular order of checks?
svaldez 2016/02/17 16:46:46 Doing it in the order of the CertID struct.
493 }
494
495 } // namespace
496
497 bool GetOCSPCertStatus(const OCSPResponseData& response_data,
498 const ParsedCertificate& issuer,
499 const ParsedCertificate& cert,
500 OCSPCertStatus* out) {
501 out->status = OCSPCertStatus::Status::UNKNOWN;
502
503 ParsedTbsCertificate tbs_cert;
504 if (!ParseTbsCertificate(cert.tbs_certificate_tlv, &tbs_cert))
505 return false;
506 ParsedTbsCertificate issuer_tbs_cert;
507 if (!ParseTbsCertificate(issuer.tbs_certificate_tlv, &issuer_tbs_cert))
508 return false;
509
510 for (const auto& response : response_data.responses) {
511 OCSPSingleResponse single_response;
512 if (!ParseOCSPSingleResponse(response, &single_response))
513 return false;
514 if (CheckCertID(single_response.cert_id_tlv, issuer_tbs_cert,
515 tbs_cert.serial_number)) {
516 *out = single_response.cert_status;
517 if (single_response.cert_status.status != OCSPCertStatus::Status::GOOD)
518 return true;
519 }
520 }
521
522 return true;
523 }
524
525 // Verify Code Below (MOVING TO SEPARATE CL)
526
527 namespace {
528
529 // Checks that the ResponderID |id| matches the certificate |cert|.
530 bool CheckResponder(const OCSPResponseData::ResponderID& id,
531 const ParsedTbsCertificate& cert) {
532 if (id.type == OCSPResponseData::ResponderType::NAME) {
533 der::Input name_rdn;
534 der::Input cert_rdn;
535 if (!der::Parser(id.name).ReadTag(der::kSequence, &name_rdn) ||
536 !der::Parser(cert.subject_tlv).ReadTag(der::kSequence, &cert_rdn))
537 return false;
538 return VerifyNameMatch(name_rdn, cert_rdn);
539 } else {
540 der::Parser parser(cert.spki_tlv);
541 der::Parser spki_parser;
542 der::BitString key_bits;
543 if (!parser.ReadSequence(&spki_parser))
544 return false;
545 if (!spki_parser.SkipTag(der::kSequence))
546 return false;
547 if (!spki_parser.ReadBitString(&key_bits))
548 return false;
549
550 der::Input key = key_bits.bytes();
551 HashValue key_hash(HASH_VALUE_SHA1);
552 base::SHA1HashBytes(key.UnsafeData(), key.Length(), key_hash.data());
553 return key_hash.Equals(id.key_hash);
554 }
555 }
556
557 } // namespace
558
559 bool VerifyOCSPResponse(const OCSPResponse& response,
560 const ParsedCertificate& issuer_cert) {
561 SimpleSignaturePolicy signature_policy(1024);
562
563 OCSPResponseData response_data;
564 if (!ParseOCSPResponseData(response.data, &response_data))
565 return false;
566
567 ParsedTbsCertificate issuer;
568 ParsedTbsCertificate responder;
569 if (!ParseTbsCertificate(issuer_cert.tbs_certificate_tlv, &issuer))
570 return false;
571
572 if (CheckResponder(response_data.responder_id, issuer)) {
573 responder = issuer;
574 } else {
575 bool found = false;
576 for (const auto& responder_cert_tlv : response.certs) {
577 ParsedCertificate responder_cert;
578 ParsedTbsCertificate tbs_cert;
579 if (!ParseCertificate(responder_cert_tlv, &responder_cert))
580 return false;
581 if (!ParseTbsCertificate(responder_cert.tbs_certificate_tlv, &tbs_cert))
582 return false;
583
584 if (CheckResponder(response_data.responder_id, tbs_cert)) {
585 found = true;
586 responder = tbs_cert;
587
588 scoped_ptr<SignatureAlgorithm> signature_algorithm =
589 SignatureAlgorithm::CreateFromDer(
590 responder_cert.signature_algorithm_tlv);
591 if (!signature_algorithm)
592 return false;
593 der::Input issuer_spki = issuer.spki_tlv;
594 if (!VerifySignedData(*signature_algorithm,
595 responder_cert.tbs_certificate_tlv,
596 responder_cert.signature_value, issuer_spki,
597 &signature_policy)) {
598 return false;
599 }
600
601 std::map<der::Input, ParsedExtension> extensions;
602 std::vector<der::Input> eku;
603 if (!ParseExtensions(responder.extensions_tlv, &extensions))
604 return false;
605 if (!ParseEKUExtension(extensions[ExtKeyUsageOid()].value, &eku))
606 return false;
607 if (std::find(eku.begin(), eku.end(), OCSPSigning()) == eku.end())
608 return false;
609 break;
610 }
611 }
612 if (!found)
613 return false;
614 }
615 return VerifySignedData(*(response.signature_algorithm), response.data,
616 response.signature, responder.spki_tlv,
617 &signature_policy);
618 }
619
620 } // namespace net
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698