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

Side by Side Diff: net/ssl/client_cert_store_chromeos_unittest.cc

Issue 424523002: Enable system NSS key slot. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix compilation of profile_io_data on !OS_CHROMEOS. Created 6 years, 4 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 | Annotate | Revision Log
« no previous file with comments | « net/ssl/client_cert_store_chromeos.cc ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "net/ssl/client_cert_store_chromeos.h" 5 #include "net/ssl/client_cert_store_chromeos.h"
6 6
7 #include <string> 7 #include <string>
8 8
9 #include "base/bind.h" 9 #include "base/bind.h"
10 #include "base/callback.h" 10 #include "base/callback.h"
11 #include "base/file_util.h" 11 #include "base/file_util.h"
12 #include "base/run_loop.h" 12 #include "base/run_loop.h"
13 #include "crypto/nss_util.h" 13 #include "crypto/nss_util.h"
14 #include "crypto/nss_util_internal.h" 14 #include "crypto/nss_util_internal.h"
15 #include "crypto/rsa_private_key.h" 15 #include "crypto/rsa_private_key.h"
16 #include "crypto/scoped_test_nss_chromeos_user.h" 16 #include "crypto/scoped_test_nss_chromeos_user.h"
17 #include "crypto/scoped_test_system_nss_key_slot.h"
17 #include "net/base/test_data_directory.h" 18 #include "net/base/test_data_directory.h"
18 #include "net/cert/cert_type.h" 19 #include "net/cert/cert_type.h"
19 #include "net/cert/x509_certificate.h" 20 #include "net/cert/x509_certificate.h"
20 #include "net/ssl/client_cert_store_unittest-inl.h" 21 #include "net/ssl/client_cert_store_unittest-inl.h"
21 #include "net/test/cert_test_util.h" 22 #include "net/test/cert_test_util.h"
22 23
23 namespace net { 24 namespace net {
24 25
25 namespace { 26 namespace {
26 27
27 bool ImportClientCertToSlot(const scoped_refptr<X509Certificate>& cert, 28 bool ImportClientCertToSlot(const scoped_refptr<X509Certificate>& cert,
28 PK11SlotInfo* slot) { 29 PK11SlotInfo* slot) {
29 std::string nickname = cert->GetDefaultNickname(USER_CERT); 30 std::string nickname = cert->GetDefaultNickname(USER_CERT);
30 { 31 {
31 crypto::AutoNSSWriteLock lock; 32 crypto::AutoNSSWriteLock lock;
32 SECStatus rv = PK11_ImportCert(slot, 33 SECStatus rv = PK11_ImportCert(slot,
33 cert->os_cert_handle(), 34 cert->os_cert_handle(),
34 CK_INVALID_HANDLE, 35 CK_INVALID_HANDLE,
35 nickname.c_str(), 36 nickname.c_str(),
36 PR_FALSE); 37 PR_FALSE);
37 if (rv != SECSuccess) { 38 if (rv != SECSuccess) {
38 LOG(ERROR) << "Could not import cert"; 39 LOG(ERROR) << "Could not import cert";
39 return false; 40 return false;
40 } 41 }
41 } 42 }
42 return true; 43 return true;
43 } 44 }
44 45
46 enum ReadFromSlot {
47 READ_FROM_SLOT_USER,
48 READ_FROM_SLOT_SYSTEM
49 };
50
51 enum SystemSlotAvailability {
52 SYSTEM_SLOT_AVAILABILITY_ENABLED,
53 SYSTEM_SLOT_AVAILABILITY_DISABLED
54 };
55
45 } // namespace 56 } // namespace
46 57
47 // Define a delegate to be used for instantiating the parameterized test set 58 // Define a delegate to be used for instantiating the parameterized test set
48 // ClientCertStoreTest. 59 // ClientCertStoreTest.
60 template <ReadFromSlot read_from,
61 SystemSlotAvailability system_slot_availability>
49 class ClientCertStoreChromeOSTestDelegate { 62 class ClientCertStoreChromeOSTestDelegate {
50 public: 63 public:
51 ClientCertStoreChromeOSTestDelegate() 64 ClientCertStoreChromeOSTestDelegate()
52 : user_("scopeduser"), 65 : user_("scopeduser"),
53 store_(user_.username_hash(), 66 store_(system_slot_availability == SYSTEM_SLOT_AVAILABILITY_ENABLED,
67 user_.username_hash(),
54 ClientCertStoreChromeOS::PasswordDelegateFactory()) { 68 ClientCertStoreChromeOS::PasswordDelegateFactory()) {
55 // Defer futher initialization and checks to SelectClientCerts, because the 69 // Defer futher initialization and checks to SelectClientCerts, because the
56 // constructor doesn't allow us to return an initialization result. Could be 70 // constructor doesn't allow us to return an initialization result. Could be
57 // cleaned up by adding an Init() function. 71 // cleaned up by adding an Init() function.
58 } 72 }
59 73
60 // Called by the ClientCertStoreTest tests. 74 // Called by the ClientCertStoreTest tests.
61 // |inpurt_certs| contains certificates to select from. Because 75 // |inpurt_certs| contains certificates to select from. Because
62 // ClientCertStoreChromeOS filters also for the right slot, we have to import 76 // ClientCertStoreChromeOS filters also for the right slot, we have to import
63 // the certs at first. 77 // the certs at first.
64 // Since the certs are imported, the store can be tested by using its public 78 // Since the certs are imported, the store can be tested by using its public
65 // interface (GetClientCerts), which will read the certs from NSS. 79 // interface (GetClientCerts), which will read the certs from NSS.
66 bool SelectClientCerts(const CertificateList& input_certs, 80 bool SelectClientCerts(const CertificateList& input_certs,
67 const SSLCertRequestInfo& cert_request_info, 81 const SSLCertRequestInfo& cert_request_info,
68 CertificateList* selected_certs) { 82 CertificateList* selected_certs) {
69 if (!user_.constructed_successfully()) { 83 if (!user_.constructed_successfully()) {
70 LOG(ERROR) << "Scoped test user DB could not be constructed."; 84 LOG(ERROR) << "Scoped test user DB could not be constructed.";
71 return false; 85 return false;
72 } 86 }
73 user_.FinishInit(); 87 user_.FinishInit();
74 88
75 crypto::ScopedPK11Slot slot( 89 crypto::ScopedPK11Slot slot;
76 crypto::GetPublicSlotForChromeOSUser(user_.username_hash())); 90 switch (read_from) {
91 case READ_FROM_SLOT_USER:
92 slot = crypto::GetPublicSlotForChromeOSUser(user_.username_hash());
93 break;
94 case READ_FROM_SLOT_SYSTEM:
95 slot.reset(PK11_ReferenceSlot(system_db_.slot()));
96 break;
97 default:
98 CHECK(false);
99 }
77 if (!slot) { 100 if (!slot) {
78 LOG(ERROR) << "Could not get the user's public slot"; 101 LOG(ERROR) << "Could not get the NSS key slot";
79 return false; 102 return false;
80 } 103 }
81 104
82 // Only user certs are considered for the cert request, which means that the 105 // Only user certs are considered for the cert request, which means that the
83 // private key must be known to NSS. Import all private keys for certs that 106 // private key must be known to NSS. Import all private keys for certs that
84 // are used througout the test. 107 // are used througout the test.
85 if (!ImportSensitiveKeyFromFile( 108 if (!ImportSensitiveKeyFromFile(
86 GetTestCertsDirectory(), "client_1.pk8", slot.get()) || 109 GetTestCertsDirectory(), "client_1.pk8", slot.get()) ||
87 !ImportSensitiveKeyFromFile( 110 !ImportSensitiveKeyFromFile(
88 GetTestCertsDirectory(), "client_2.pk8", slot.get())) { 111 GetTestCertsDirectory(), "client_2.pk8", slot.get())) {
89 return false; 112 return false;
90 } 113 }
91 114
92 for (CertificateList::const_iterator it = input_certs.begin(); 115 for (CertificateList::const_iterator it = input_certs.begin();
93 it != input_certs.end(); 116 it != input_certs.end();
94 ++it) { 117 ++it) {
95 if (!ImportClientCertToSlot(*it, slot.get())) 118 if (!ImportClientCertToSlot(*it, slot.get()))
96 return false; 119 return false;
97 } 120 }
98 base::RunLoop run_loop; 121 base::RunLoop run_loop;
99 store_.GetClientCerts( 122 store_.GetClientCerts(
100 cert_request_info, selected_certs, run_loop.QuitClosure()); 123 cert_request_info, selected_certs, run_loop.QuitClosure());
101 run_loop.Run(); 124 run_loop.Run();
102 return true; 125 return true;
103 } 126 }
104 127
105 private: 128 private:
106 crypto::ScopedTestNSSChromeOSUser user_; 129 crypto::ScopedTestNSSChromeOSUser user_;
130 crypto::ScopedTestSystemNSSKeySlot system_db_;
107 ClientCertStoreChromeOS store_; 131 ClientCertStoreChromeOS store_;
108 }; 132 };
109 133
110 // ClientCertStoreChromeOS derives from ClientCertStoreNSS and delegates the 134 // ClientCertStoreChromeOS derives from ClientCertStoreNSS and delegates the
111 // filtering by issuer to that base class. 135 // filtering by issuer to that base class.
112 // To verify that this delegation is functional, run the same filtering tests as 136 // To verify that this delegation is functional, run the same filtering tests as
113 // for the other implementations. These tests are defined in 137 // for the other implementations. These tests are defined in
114 // client_cert_store_unittest-inl.h and are instantiated for each platform. 138 // client_cert_store_unittest-inl.h and are instantiated for each platform.
115 INSTANTIATE_TYPED_TEST_CASE_P(ChromeOS, 139
140 // In this case, all requested certs are read from the user's slot and the
141 // system slot is not enabled in the store.
142 typedef ClientCertStoreChromeOSTestDelegate<READ_FROM_SLOT_USER,
143 SYSTEM_SLOT_AVAILABILITY_DISABLED>
144 DelegateReadUserDisableSystem;
145 INSTANTIATE_TYPED_TEST_CASE_P(ChromeOS_ReadUserDisableSystem,
116 ClientCertStoreTest, 146 ClientCertStoreTest,
117 ClientCertStoreChromeOSTestDelegate); 147 DelegateReadUserDisableSystem);
148
149 // In this case, all requested certs are read from the user's slot and the
150 // system slot is enabled in the store.
151 typedef ClientCertStoreChromeOSTestDelegate<READ_FROM_SLOT_USER,
152 SYSTEM_SLOT_AVAILABILITY_ENABLED>
153 DelegateReadUserEnableSystem;
154 INSTANTIATE_TYPED_TEST_CASE_P(ChromeOS_ReadUserEnableSystem,
155 ClientCertStoreTest,
156 DelegateReadUserEnableSystem);
157
158 // In this case, all requested certs are read from the system slot, therefore
159 // the system slot is enabled in the store.
160 typedef ClientCertStoreChromeOSTestDelegate<READ_FROM_SLOT_SYSTEM,
161 SYSTEM_SLOT_AVAILABILITY_ENABLED>
162 DelegateReadSystem;
163 INSTANTIATE_TYPED_TEST_CASE_P(ChromeOS_ReadSystem,
164 ClientCertStoreTest,
165 DelegateReadSystem);
118 166
119 class ClientCertStoreChromeOSTest : public ::testing::Test { 167 class ClientCertStoreChromeOSTest : public ::testing::Test {
120 public: 168 public:
121 scoped_refptr<X509Certificate> ImportCertForUser( 169 scoped_refptr<X509Certificate> ImportCertToSlot(
122 const std::string& username_hash,
123 const std::string& cert_filename, 170 const std::string& cert_filename,
124 const std::string& key_filename) { 171 const std::string& key_filename,
125 crypto::ScopedPK11Slot slot( 172 PK11SlotInfo* slot) {
126 crypto::GetPublicSlotForChromeOSUser(username_hash)); 173 if (!ImportSensitiveKeyFromFile(
127 if (!slot) { 174 GetTestCertsDirectory(), key_filename, slot)) {
128 LOG(ERROR) << "No slot for user " << username_hash; 175 LOG(ERROR) << "Could not import private key from file " << key_filename;
129 return NULL; 176 return NULL;
130 } 177 }
131 178
132 if (!ImportSensitiveKeyFromFile(
133 GetTestCertsDirectory(), key_filename, slot.get())) {
134 LOG(ERROR) << "Could not import private key for user " << username_hash;
135 return NULL;
136 }
137
138 scoped_refptr<X509Certificate> cert( 179 scoped_refptr<X509Certificate> cert(
139 ImportCertFromFile(GetTestCertsDirectory(), cert_filename)); 180 ImportCertFromFile(GetTestCertsDirectory(), cert_filename));
140 181
141 if (!cert) { 182 if (!cert) {
142 LOG(ERROR) << "Failed to parse cert from file " << cert_filename; 183 LOG(ERROR) << "Failed to parse cert from file " << cert_filename;
143 return NULL; 184 return NULL;
144 } 185 }
145 186
146 if (!ImportClientCertToSlot(cert, slot.get())) 187 if (!ImportClientCertToSlot(cert, slot))
147 return NULL; 188 return NULL;
148 189
149 // |cert| continues to point to the original X509Certificate before the 190 // |cert| continues to point to the original X509Certificate before the
150 // import to |slot|. However this should not make a difference for this 191 // import to |slot|. However this should not make a difference for this
151 // test. 192 // test.
152 return cert; 193 return cert;
153 } 194 }
195
196 scoped_refptr<X509Certificate> ImportCertForUser(
197 const std::string& username_hash,
198 const std::string& cert_filename,
199 const std::string& key_filename) {
200 crypto::ScopedPK11Slot slot(
201 crypto::GetPublicSlotForChromeOSUser(username_hash));
202 if (!slot) {
203 LOG(ERROR) << "No slot for user " << username_hash;
204 return NULL;
205 }
206
207 return ImportCertToSlot(cert_filename, key_filename, slot.get());
208 }
209
154 }; 210 };
155 211
156 // Ensure that cert requests, that are started before the user's NSS DB is 212 // Ensure that cert requests, that are started before the user's NSS DB is
157 // initialized, will wait for the initialization and succeed afterwards. 213 // initialized, will wait for the initialization and succeed afterwards.
158 TEST_F(ClientCertStoreChromeOSTest, RequestWaitsForNSSInitAndSucceeds) { 214 TEST_F(ClientCertStoreChromeOSTest, RequestWaitsForNSSInitAndSucceeds) {
159 crypto::ScopedTestNSSChromeOSUser user("scopeduser"); 215 crypto::ScopedTestNSSChromeOSUser user("scopeduser");
160 ASSERT_TRUE(user.constructed_successfully()); 216 ASSERT_TRUE(user.constructed_successfully());
217
218 crypto::ScopedTestSystemNSSKeySlot system_slot;
219
161 ClientCertStoreChromeOS store( 220 ClientCertStoreChromeOS store(
162 user.username_hash(), ClientCertStoreChromeOS::PasswordDelegateFactory()); 221 true /* use system slot */,
222 user.username_hash(),
223 ClientCertStoreChromeOS::PasswordDelegateFactory());
163 scoped_refptr<X509Certificate> cert_1( 224 scoped_refptr<X509Certificate> cert_1(
164 ImportCertForUser(user.username_hash(), "client_1.pem", "client_1.pk8")); 225 ImportCertForUser(user.username_hash(), "client_1.pem", "client_1.pk8"));
165 ASSERT_TRUE(cert_1); 226 ASSERT_TRUE(cert_1);
166 227
167 // Request any client certificate, which is expected to match client_1. 228 // Request any client certificate, which is expected to match client_1.
168 scoped_refptr<SSLCertRequestInfo> request_all(new SSLCertRequestInfo()); 229 scoped_refptr<SSLCertRequestInfo> request_all(new SSLCertRequestInfo());
169 230
170 base::RunLoop run_loop; 231 base::RunLoop run_loop;
171 store.GetClientCerts( 232 store.GetClientCerts(
172 *request_all, &request_all->client_certs, run_loop.QuitClosure()); 233 *request_all, &request_all->client_certs, run_loop.QuitClosure());
(...skipping 14 matching lines...) Expand all
187 ASSERT_EQ(1u, request_all->client_certs.size()); 248 ASSERT_EQ(1u, request_all->client_certs.size());
188 } 249 }
189 250
190 // Ensure that cert requests, that are started after the user's NSS DB was 251 // Ensure that cert requests, that are started after the user's NSS DB was
191 // initialized, will succeed. 252 // initialized, will succeed.
192 TEST_F(ClientCertStoreChromeOSTest, RequestsAfterNSSInitSucceed) { 253 TEST_F(ClientCertStoreChromeOSTest, RequestsAfterNSSInitSucceed) {
193 crypto::ScopedTestNSSChromeOSUser user("scopeduser"); 254 crypto::ScopedTestNSSChromeOSUser user("scopeduser");
194 ASSERT_TRUE(user.constructed_successfully()); 255 ASSERT_TRUE(user.constructed_successfully());
195 user.FinishInit(); 256 user.FinishInit();
196 257
258 crypto::ScopedTestSystemNSSKeySlot system_slot;
259
197 ClientCertStoreChromeOS store( 260 ClientCertStoreChromeOS store(
198 user.username_hash(), ClientCertStoreChromeOS::PasswordDelegateFactory()); 261 true /* use system slot */,
262 user.username_hash(),
263 ClientCertStoreChromeOS::PasswordDelegateFactory());
199 scoped_refptr<X509Certificate> cert_1( 264 scoped_refptr<X509Certificate> cert_1(
200 ImportCertForUser(user.username_hash(), "client_1.pem", "client_1.pk8")); 265 ImportCertForUser(user.username_hash(), "client_1.pem", "client_1.pk8"));
201 ASSERT_TRUE(cert_1); 266 ASSERT_TRUE(cert_1);
202 267
203 scoped_refptr<SSLCertRequestInfo> request_all(new SSLCertRequestInfo()); 268 scoped_refptr<SSLCertRequestInfo> request_all(new SSLCertRequestInfo());
204 269
205 base::RunLoop run_loop; 270 base::RunLoop run_loop;
206 store.GetClientCerts( 271 store.GetClientCerts(
207 *request_all, &request_all->client_certs, run_loop.QuitClosure()); 272 *request_all, &request_all->client_certs, run_loop.QuitClosure());
208 run_loop.Run(); 273 run_loop.Run();
209 274
210 ASSERT_EQ(1u, request_all->client_certs.size()); 275 ASSERT_EQ(1u, request_all->client_certs.size());
211 } 276 }
212 277
213 // This verifies that a request in the context of User1 doesn't see certificates 278 // This verifies that a request in the context of User1 doesn't see certificates
214 // of User2, and the other way round. We check both directions, to ensure that 279 // of User2, and the other way round. We check both directions, to ensure that
215 // the behavior doesn't depend on initialization order of the DBs, for example. 280 // the behavior doesn't depend on initialization order of the DBs, for example.
216 TEST_F(ClientCertStoreChromeOSTest, RequestDoesCrossReadSecondDB) { 281 TEST_F(ClientCertStoreChromeOSTest, RequestDoesCrossReadOtherUserDB) {
217 crypto::ScopedTestNSSChromeOSUser user1("scopeduser1"); 282 crypto::ScopedTestNSSChromeOSUser user1("scopeduser1");
218 ASSERT_TRUE(user1.constructed_successfully()); 283 ASSERT_TRUE(user1.constructed_successfully());
219 crypto::ScopedTestNSSChromeOSUser user2("scopeduser2"); 284 crypto::ScopedTestNSSChromeOSUser user2("scopeduser2");
220 ASSERT_TRUE(user2.constructed_successfully()); 285 ASSERT_TRUE(user2.constructed_successfully());
221 286
222 user1.FinishInit(); 287 user1.FinishInit();
223 user2.FinishInit(); 288 user2.FinishInit();
224 289
290 crypto::ScopedTestSystemNSSKeySlot system_slot;
291
225 ClientCertStoreChromeOS store1( 292 ClientCertStoreChromeOS store1(
293 true /* use system slot */,
226 user1.username_hash(), 294 user1.username_hash(),
227 ClientCertStoreChromeOS::PasswordDelegateFactory()); 295 ClientCertStoreChromeOS::PasswordDelegateFactory());
228 ClientCertStoreChromeOS store2( 296 ClientCertStoreChromeOS store2(
297 true /* use system slot */,
229 user2.username_hash(), 298 user2.username_hash(),
230 ClientCertStoreChromeOS::PasswordDelegateFactory()); 299 ClientCertStoreChromeOS::PasswordDelegateFactory());
231 300
232 scoped_refptr<X509Certificate> cert_1( 301 scoped_refptr<X509Certificate> cert_1(
233 ImportCertForUser(user1.username_hash(), "client_1.pem", "client_1.pk8")); 302 ImportCertForUser(user1.username_hash(), "client_1.pem", "client_1.pk8"));
234 ASSERT_TRUE(cert_1); 303 ASSERT_TRUE(cert_1);
235 scoped_refptr<X509Certificate> cert_2( 304 scoped_refptr<X509Certificate> cert_2(
236 ImportCertForUser(user2.username_hash(), "client_2.pem", "client_2.pk8")); 305 ImportCertForUser(user2.username_hash(), "client_2.pem", "client_2.pk8"));
237 ASSERT_TRUE(cert_2); 306 ASSERT_TRUE(cert_2);
238 307
(...skipping 13 matching lines...) Expand all
252 321
253 // store1 should only return certs of user1, namely cert_1. 322 // store1 should only return certs of user1, namely cert_1.
254 ASSERT_EQ(1u, selected_certs1.size()); 323 ASSERT_EQ(1u, selected_certs1.size());
255 EXPECT_TRUE(cert_1->Equals(selected_certs1[0])); 324 EXPECT_TRUE(cert_1->Equals(selected_certs1[0]));
256 325
257 // store2 should only return certs of user2, namely cert_2. 326 // store2 should only return certs of user2, namely cert_2.
258 ASSERT_EQ(1u, selected_certs2.size()); 327 ASSERT_EQ(1u, selected_certs2.size());
259 EXPECT_TRUE(cert_2->Equals(selected_certs2[0])); 328 EXPECT_TRUE(cert_2->Equals(selected_certs2[0]));
260 } 329 }
261 330
331 // This verifies that a request in the context of User1 doesn't see certificates
332 // of the system store if the system store is disabled.
333 TEST_F(ClientCertStoreChromeOSTest, RequestDoesCrossReadSystemDB) {
334 crypto::ScopedTestNSSChromeOSUser user1("scopeduser1");
335 ASSERT_TRUE(user1.constructed_successfully());
336
337 user1.FinishInit();
338
339 crypto::ScopedTestSystemNSSKeySlot system_slot;
340
341 ClientCertStoreChromeOS store(
342 false /* do not use system slot */,
343 user1.username_hash(),
344 ClientCertStoreChromeOS::PasswordDelegateFactory());
345
346 scoped_refptr<X509Certificate> cert_1(
347 ImportCertForUser(user1.username_hash(), "client_1.pem", "client_1.pk8"));
348 ASSERT_TRUE(cert_1);
349 scoped_refptr<X509Certificate> cert_2(
350 ImportCertToSlot("client_2.pem", "client_2.pk8", system_slot.slot()));
351 ASSERT_TRUE(cert_2);
352
353 scoped_refptr<SSLCertRequestInfo> request_all(new SSLCertRequestInfo());
354
355 base::RunLoop run_loop;
356
357 CertificateList selected_certs;
358 store.GetClientCerts(*request_all, &selected_certs, run_loop.QuitClosure());
359
360 run_loop.Run();
361
362 // store should only return certs of the user, namely cert_1.
363 ASSERT_EQ(1u, selected_certs.size());
364 EXPECT_TRUE(cert_1->Equals(selected_certs[0]));
365 }
366
262 } // namespace net 367 } // namespace net
OLDNEW
« no previous file with comments | « net/ssl/client_cert_store_chromeos.cc ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698