1a5d528c7SSumit Garg // SPDX-License-Identifier: BSD-2-Clause
2a5d528c7SSumit Garg /*
3e27e865aSEtienne Carriere * Copyright (C) 2018-2022, Linaro Limited
4a5d528c7SSumit Garg */
5a5d528c7SSumit Garg
6a5d528c7SSumit Garg /*
7a5d528c7SSumit Garg * Developerbox doesn't provide a hardware based true random number
8a5d528c7SSumit Garg * generator. So this pseudo TA provides a good source of entropy using
9a5d528c7SSumit Garg * noise from 7 thermal sensors. Its suitable for entropy required
10a5d528c7SSumit Garg * during boot, seeding kernel entropy pool, cryptographic use etc.
11a5d528c7SSumit Garg *
12a5d528c7SSumit Garg * Assumption
13a5d528c7SSumit Garg * ==========
14a5d528c7SSumit Garg *
15a5d528c7SSumit Garg * We have assumed the entropy of the sensor is better than 8 bits per
16a5d528c7SSumit Garg * 14 sensor readings. This entropy estimate is based on our simple
17a5d528c7SSumit Garg * minimal entropy estimates done on 2.1G bytes of raw samples collected
18a5d528c7SSumit Garg * from thermal sensors.
19a5d528c7SSumit Garg *
20a5d528c7SSumit Garg * We believe our estimate to be conservative and have designed to
21a5d528c7SSumit Garg * health tests to trigger if a sensor does not achieve at least
22a5d528c7SSumit Garg * 8 bits in 16 sensor reading (we use 16 rather than 14 to prevent
23a5d528c7SSumit Garg * spurious failures on edge cases).
24a5d528c7SSumit Garg *
25a5d528c7SSumit Garg * Theory of operation
26a5d528c7SSumit Garg * ===================
27a5d528c7SSumit Garg *
28a5d528c7SSumit Garg * This routine uses secure timer interrupt to sample raw thermal sensor
29a5d528c7SSumit Garg * readings. As thermal sensor refresh rate is every 2ms, so interrupt
30a5d528c7SSumit Garg * fires every 2ms. It implements continuous health test counting rising
31a5d528c7SSumit Garg * and falling edges to report if sensors fail to provide entropy.
32a5d528c7SSumit Garg *
33a5d528c7SSumit Garg * It uses vetted conditioner as SHA512/256 (approved hash algorithm)
34a5d528c7SSumit Garg * to condense entropy. As per NIST.SP.800-90B spec, to get full entropy
35a5d528c7SSumit Garg * from vetted conditioner, we need to supply double of input entropy.
36a5d528c7SSumit Garg * According to assumption above and requirement for vetted conditioner,
37a5d528c7SSumit Garg * we need to supply 28 raw sensor readings to get 1 byte of full
38a5d528c7SSumit Garg * entropy as output. So for 32 bytes of conditioner output, we need to
39a5d528c7SSumit Garg * supply 896 bytes of raw sensor readings.
40a5d528c7SSumit Garg *
41a5d528c7SSumit Garg * Interfaces -> Input
42a5d528c7SSumit Garg * -------------------
43a5d528c7SSumit Garg *
44a5d528c7SSumit Garg * void rng_collect_entropy(void);
45a5d528c7SSumit Garg *
46a5d528c7SSumit Garg * Called as part of secure timer interrupt handler to sample raw
47a5d528c7SSumit Garg * thermal sensor readings and add entropy to the pool.
48a5d528c7SSumit Garg *
49a5d528c7SSumit Garg * Interfaces -> Output
50a5d528c7SSumit Garg * --------------------
51a5d528c7SSumit Garg *
52a5d528c7SSumit Garg * TEE_Result rng_get_entropy(uint32_t types,
53a5d528c7SSumit Garg * TEE_Param params[TEE_NUM_PARAMS]);
54a5d528c7SSumit Garg *
55a5d528c7SSumit Garg * Invoke command to expose an entropy interface to normal world.
56a5d528c7SSumit Garg *
57a5d528c7SSumit Garg * Testing
58a5d528c7SSumit Garg * =======
59a5d528c7SSumit Garg *
60a5d528c7SSumit Garg * Passes FIPS 140-2 rngtest.
61a5d528c7SSumit Garg *
62a5d528c7SSumit Garg * Limitations
63a5d528c7SSumit Garg * ===========
64a5d528c7SSumit Garg *
656e0800cfSSumit Garg * Output rate is limited to approx. 125 bytes per second.
66a5d528c7SSumit Garg *
67a5d528c7SSumit Garg * Our entropy estimation was not reached using any approved or
68a5d528c7SSumit Garg * published estimation framework such as NIST.SP.800-90B and was tested
69a5d528c7SSumit Garg * on a very small set of physical samples. Instead we have adopted what
70a5d528c7SSumit Garg * we believe to be a conservative estimate and partnered it with a
71a5d528c7SSumit Garg * fairly agressive health check.
72a5d528c7SSumit Garg *
73a5d528c7SSumit Garg * Generating the SHA512/256 hash takes 24uS and will be run by an
74a5d528c7SSumit Garg * interrupt handler that pre-empts the normal world.
75a5d528c7SSumit Garg */
76a5d528c7SSumit Garg
77a5d528c7SSumit Garg #include <crypto/crypto.h>
78a5d528c7SSumit Garg #include <kernel/delay.h>
79a5d528c7SSumit Garg #include <kernel/pseudo_ta.h>
80a5d528c7SSumit Garg #include <kernel/spinlock.h>
81a5d528c7SSumit Garg #include <kernel/timer.h>
82a5d528c7SSumit Garg #include <mm/core_memprot.h>
83a5d528c7SSumit Garg #include <io.h>
84e27e865aSEtienne Carriere #include <pta_rng.h>
85a5d528c7SSumit Garg #include <string.h>
86a9d09adaSEtienne Carriere
87a9d09adaSEtienne Carriere #include "synquacer_rng_pta.h"
88a5d528c7SSumit Garg
89a5d528c7SSumit Garg #define PTA_NAME "rng.pta"
90a5d528c7SSumit Garg
91a5d528c7SSumit Garg #define THERMAL_SENSOR_BASE0 0x54190800
92a5d528c7SSumit Garg #define THERMAL_SENSOR_OFFSET 0x80
93a5d528c7SSumit Garg #define NUM_SENSORS 7
94a5d528c7SSumit Garg #define NUM_SLOTS ((NUM_SENSORS * 2) - 1)
95a5d528c7SSumit Garg
96a5d528c7SSumit Garg #define TEMP_DATA_REG_OFFSET 0x34
97a5d528c7SSumit Garg
98a5d528c7SSumit Garg #define ENTROPY_POOL_SIZE 4096
99a5d528c7SSumit Garg
100a5d528c7SSumit Garg #define SENSOR_DATA_SIZE 128
101a5d528c7SSumit Garg #define CONDITIONER_PAYLOAD (SENSOR_DATA_SIZE * NUM_SENSORS)
102a5d528c7SSumit Garg
103a5d528c7SSumit Garg /*
104a5d528c7SSumit Garg * The health test monitors each sensor's least significant bit and counts
105a5d528c7SSumit Garg * the number of rising and falling edges. It verifies that both counts
106a5d528c7SSumit Garg * lie within interval of between 12.5% and 37.5% of the samples.
107a5d528c7SSumit Garg * For true random data with 8 bits of entropy per byte, both counts would
108a5d528c7SSumit Garg * be close to 25%.
109a5d528c7SSumit Garg */
110a5d528c7SSumit Garg #define MAX_BIT_FLIP_EDGE_COUNT ((3 * SENSOR_DATA_SIZE) / 8)
111a5d528c7SSumit Garg #define MIN_BIT_FLIP_EDGE_COUNT (SENSOR_DATA_SIZE / 8)
112a5d528c7SSumit Garg
113a5d528c7SSumit Garg static uint8_t entropy_pool[ENTROPY_POOL_SIZE] = {0};
114a5d528c7SSumit Garg static uint32_t entropy_size;
115a5d528c7SSumit Garg
116a5d528c7SSumit Garg static uint8_t sensors_data[NUM_SLOTS][SENSOR_DATA_SIZE] = {0};
117a5d528c7SSumit Garg static uint8_t sensors_data_slot_idx;
118a5d528c7SSumit Garg static uint8_t sensors_data_idx;
119a5d528c7SSumit Garg
120a5d528c7SSumit Garg static uint32_t health_test_fail_cnt;
121a5d528c7SSumit Garg static uint32_t health_test_cnt;
122a5d528c7SSumit Garg
123a5d528c7SSumit Garg static unsigned int entropy_lock = SPINLOCK_UNLOCK;
124a5d528c7SSumit Garg
pool_add_entropy(uint8_t * entropy,uint32_t size)125a5d528c7SSumit Garg static void pool_add_entropy(uint8_t *entropy, uint32_t size)
126a5d528c7SSumit Garg {
127622eef2dSEtienne Carriere uint32_t copy_size = 0;
128a5d528c7SSumit Garg
129a5d528c7SSumit Garg if (entropy_size >= ENTROPY_POOL_SIZE)
130a5d528c7SSumit Garg return;
131a5d528c7SSumit Garg
132a5d528c7SSumit Garg if ((ENTROPY_POOL_SIZE - entropy_size) >= size)
133a5d528c7SSumit Garg copy_size = size;
134a5d528c7SSumit Garg else
135a5d528c7SSumit Garg copy_size = ENTROPY_POOL_SIZE - entropy_size;
136a5d528c7SSumit Garg
137a5d528c7SSumit Garg memcpy((entropy_pool + entropy_size), entropy, copy_size);
138a5d528c7SSumit Garg
139a5d528c7SSumit Garg entropy_size += copy_size;
140a5d528c7SSumit Garg }
141a5d528c7SSumit Garg
pool_get_entropy(uint8_t * buf,uint32_t size)142a5d528c7SSumit Garg static void pool_get_entropy(uint8_t *buf, uint32_t size)
143a5d528c7SSumit Garg {
144622eef2dSEtienne Carriere uint32_t off = 0;
145a5d528c7SSumit Garg
146a5d528c7SSumit Garg if (size > entropy_size)
147a5d528c7SSumit Garg return;
148a5d528c7SSumit Garg
149a5d528c7SSumit Garg off = entropy_size - size;
150a5d528c7SSumit Garg
151a5d528c7SSumit Garg memcpy(buf, &entropy_pool[off], size);
152a5d528c7SSumit Garg entropy_size -= size;
153a5d528c7SSumit Garg }
154a5d528c7SSumit Garg
health_test(uint8_t sensor_id)155a5d528c7SSumit Garg static bool health_test(uint8_t sensor_id)
156a5d528c7SSumit Garg {
157622eef2dSEtienne Carriere uint32_t falling_edge_count = 0;
158622eef2dSEtienne Carriere uint32_t rising_edge_count = 0;
159622eef2dSEtienne Carriere uint32_t lo_edge_count = 0;
160622eef2dSEtienne Carriere uint32_t hi_edge_count = 0;
161622eef2dSEtienne Carriere uint32_t i = 0;
162a5d528c7SSumit Garg
163a5d528c7SSumit Garg for (i = 0; i < (SENSOR_DATA_SIZE - 1); i++) {
164a5d528c7SSumit Garg if ((sensors_data[sensor_id][i] ^
165a5d528c7SSumit Garg sensors_data[sensor_id][i + 1]) & 0x1) {
166a5d528c7SSumit Garg falling_edge_count += (sensors_data[sensor_id][i] &
167a5d528c7SSumit Garg 0x1);
168a5d528c7SSumit Garg rising_edge_count += (sensors_data[sensor_id][i + 1] &
169a5d528c7SSumit Garg 0x1);
170a5d528c7SSumit Garg }
171a5d528c7SSumit Garg }
172a5d528c7SSumit Garg
173a5d528c7SSumit Garg lo_edge_count = rising_edge_count < falling_edge_count ?
174a5d528c7SSumit Garg rising_edge_count : falling_edge_count;
175a5d528c7SSumit Garg hi_edge_count = rising_edge_count < falling_edge_count ?
176a5d528c7SSumit Garg falling_edge_count : rising_edge_count;
177a5d528c7SSumit Garg
178a5d528c7SSumit Garg return (lo_edge_count >= MIN_BIT_FLIP_EDGE_COUNT) &&
179a5d528c7SSumit Garg (hi_edge_count <= MAX_BIT_FLIP_EDGE_COUNT);
180a5d528c7SSumit Garg }
181a5d528c7SSumit Garg
pool_check_add_entropy(void)182a5d528c7SSumit Garg static uint8_t pool_check_add_entropy(void)
183a5d528c7SSumit Garg {
184622eef2dSEtienne Carriere uint32_t i = 0;
185a5d528c7SSumit Garg uint8_t entropy_sha512_256[TEE_SHA256_HASH_SIZE];
186a5d528c7SSumit Garg uint8_t pool_status = 0;
187622eef2dSEtienne Carriere TEE_Result res = TEE_ERROR_GENERIC;
188a5d528c7SSumit Garg
189a5d528c7SSumit Garg for (i = 0; i < NUM_SENSORS; i++) {
190a5d528c7SSumit Garg /* Check if particular sensor data passes health test */
191a5d528c7SSumit Garg if (health_test(sensors_data_slot_idx) == true) {
192a5d528c7SSumit Garg sensors_data_slot_idx++;
193a5d528c7SSumit Garg } else {
194a5d528c7SSumit Garg health_test_fail_cnt++;
195a5d528c7SSumit Garg memmove(sensors_data[sensors_data_slot_idx],
196a5d528c7SSumit Garg sensors_data[sensors_data_slot_idx + 1],
197a5d528c7SSumit Garg (SENSOR_DATA_SIZE * (NUM_SENSORS - i - 1)));
198a5d528c7SSumit Garg }
199a5d528c7SSumit Garg }
200a5d528c7SSumit Garg
201a5d528c7SSumit Garg health_test_cnt += NUM_SENSORS;
202a5d528c7SSumit Garg
203a5d528c7SSumit Garg /* Check if sensors_data have enough pass data for conditioning */
204a5d528c7SSumit Garg if (sensors_data_slot_idx >= NUM_SENSORS) {
205a5d528c7SSumit Garg /*
206a5d528c7SSumit Garg * Use vetted conditioner SHA512/256 as per
207a5d528c7SSumit Garg * NIST.SP.800-90B to condition raw data from entropy
208a5d528c7SSumit Garg * source.
209a5d528c7SSumit Garg */
210a5d528c7SSumit Garg sensors_data_slot_idx -= NUM_SENSORS;
211a5d528c7SSumit Garg res = hash_sha512_256_compute(entropy_sha512_256,
212a5d528c7SSumit Garg sensors_data[sensors_data_slot_idx],
213a5d528c7SSumit Garg CONDITIONER_PAYLOAD);
214a5d528c7SSumit Garg if (res == TEE_SUCCESS)
215a5d528c7SSumit Garg pool_add_entropy(entropy_sha512_256,
216a5d528c7SSumit Garg TEE_SHA256_HASH_SIZE);
217a5d528c7SSumit Garg }
218a5d528c7SSumit Garg
219a5d528c7SSumit Garg if (entropy_size >= ENTROPY_POOL_SIZE)
220a5d528c7SSumit Garg pool_status = 1;
221a5d528c7SSumit Garg
222a5d528c7SSumit Garg return pool_status;
223a5d528c7SSumit Garg }
224a5d528c7SSumit Garg
rng_collect_entropy(void)225a5d528c7SSumit Garg void rng_collect_entropy(void)
226a5d528c7SSumit Garg {
227622eef2dSEtienne Carriere uint8_t i = 0;
228622eef2dSEtienne Carriere void *vaddr = 0;
229622eef2dSEtienne Carriere uint8_t pool_full = 0;
230*7c9a7b0cSEtienne Carriere uint32_t exceptions = 0;
231a5d528c7SSumit Garg
232*7c9a7b0cSEtienne Carriere exceptions = cpu_spin_lock_xsave(&entropy_lock);
233a5d528c7SSumit Garg
234a5d528c7SSumit Garg for (i = 0; i < NUM_SENSORS; i++) {
235a5d528c7SSumit Garg vaddr = phys_to_virt_io(THERMAL_SENSOR_BASE0 +
236a5d528c7SSumit Garg (THERMAL_SENSOR_OFFSET * i) +
237c2e4eb43SAnton Rybakov TEMP_DATA_REG_OFFSET,
238c2e4eb43SAnton Rybakov sizeof(uint32_t));
239a5d528c7SSumit Garg sensors_data[sensors_data_slot_idx + i][sensors_data_idx] =
2408bf2b291SEtienne Carriere (uint8_t)io_read32((vaddr_t)vaddr);
241a5d528c7SSumit Garg }
242a5d528c7SSumit Garg
243a5d528c7SSumit Garg sensors_data_idx++;
244a5d528c7SSumit Garg
245a5d528c7SSumit Garg if (sensors_data_idx >= SENSOR_DATA_SIZE) {
246a5d528c7SSumit Garg pool_full = pool_check_add_entropy();
247a5d528c7SSumit Garg sensors_data_idx = 0;
248a5d528c7SSumit Garg }
249a5d528c7SSumit Garg
250a5d528c7SSumit Garg if (pool_full)
251a5d528c7SSumit Garg generic_timer_stop();
252a5d528c7SSumit Garg
253*7c9a7b0cSEtienne Carriere cpu_spin_unlock_xrestore(&entropy_lock, exceptions);
254a5d528c7SSumit Garg }
255a5d528c7SSumit Garg
rng_get_entropy(uint32_t types,TEE_Param params[TEE_NUM_PARAMS])256a5d528c7SSumit Garg static TEE_Result rng_get_entropy(uint32_t types,
257a5d528c7SSumit Garg TEE_Param params[TEE_NUM_PARAMS])
258a5d528c7SSumit Garg {
259a5d528c7SSumit Garg uint8_t *e = NULL;
260622eef2dSEtienne Carriere uint32_t rq_size = 0;
261622eef2dSEtienne Carriere uint32_t pool_size = 0;
262622eef2dSEtienne Carriere uint32_t exceptions = 0;
263a5d528c7SSumit Garg TEE_Result res = TEE_SUCCESS;
264a5d528c7SSumit Garg
265a5d528c7SSumit Garg if (types != TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INOUT,
266a5d528c7SSumit Garg TEE_PARAM_TYPE_NONE,
267a5d528c7SSumit Garg TEE_PARAM_TYPE_NONE,
268a5d528c7SSumit Garg TEE_PARAM_TYPE_NONE)) {
269a5d528c7SSumit Garg EMSG("bad parameters types: 0x%" PRIx32, types);
270a5d528c7SSumit Garg return TEE_ERROR_BAD_PARAMETERS;
271a5d528c7SSumit Garg }
272a5d528c7SSumit Garg
273a5d528c7SSumit Garg rq_size = params[0].memref.size;
274a5d528c7SSumit Garg
275a5d528c7SSumit Garg if ((rq_size == 0) || (rq_size > ENTROPY_POOL_SIZE))
276a5d528c7SSumit Garg return TEE_ERROR_NOT_SUPPORTED;
277a5d528c7SSumit Garg
278a5d528c7SSumit Garg e = (uint8_t *)params[0].memref.buffer;
279a5d528c7SSumit Garg if (!e)
280a5d528c7SSumit Garg return TEE_ERROR_BAD_PARAMETERS;
281a5d528c7SSumit Garg
282*7c9a7b0cSEtienne Carriere exceptions = cpu_spin_lock_xsave(&entropy_lock);
283a5d528c7SSumit Garg
284a5d528c7SSumit Garg /*
285a5d528c7SSumit Garg * Report health test failure to normal world in case fail count
286a5d528c7SSumit Garg * exceeds 1% of pass count.
287a5d528c7SSumit Garg */
288a5d528c7SSumit Garg if (health_test_fail_cnt > ((health_test_cnt + 100) / 100)) {
289a5d528c7SSumit Garg res = TEE_ERROR_HEALTH_TEST_FAIL;
290a5d528c7SSumit Garg params[0].memref.size = 0;
291a5d528c7SSumit Garg health_test_cnt = 0;
292a5d528c7SSumit Garg health_test_fail_cnt = 0;
293a5d528c7SSumit Garg goto exit;
294a5d528c7SSumit Garg }
295a5d528c7SSumit Garg
296a5d528c7SSumit Garg pool_size = entropy_size;
297a5d528c7SSumit Garg
298a5d528c7SSumit Garg if (pool_size < rq_size) {
299a5d528c7SSumit Garg params[0].memref.size = pool_size;
300a5d528c7SSumit Garg pool_get_entropy(e, pool_size);
301a5d528c7SSumit Garg } else {
302a5d528c7SSumit Garg params[0].memref.size = rq_size;
303a5d528c7SSumit Garg pool_get_entropy(e, rq_size);
304a5d528c7SSumit Garg }
305a5d528c7SSumit Garg
306a5d528c7SSumit Garg exit:
307a5d528c7SSumit Garg /* Enable timer FIQ to fetch entropy */
308a5d528c7SSumit Garg generic_timer_start(TIMER_PERIOD_MS);
309a5d528c7SSumit Garg
310*7c9a7b0cSEtienne Carriere cpu_spin_unlock_xrestore(&entropy_lock, exceptions);
311a5d528c7SSumit Garg
312a5d528c7SSumit Garg return res;
313a5d528c7SSumit Garg }
314a5d528c7SSumit Garg
rng_get_info(uint32_t types,TEE_Param params[TEE_NUM_PARAMS])3156e0800cfSSumit Garg static TEE_Result rng_get_info(uint32_t types,
3166e0800cfSSumit Garg TEE_Param params[TEE_NUM_PARAMS])
3176e0800cfSSumit Garg {
3186e0800cfSSumit Garg if (types != TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_OUTPUT,
3196e0800cfSSumit Garg TEE_PARAM_TYPE_NONE,
3206e0800cfSSumit Garg TEE_PARAM_TYPE_NONE,
3216e0800cfSSumit Garg TEE_PARAM_TYPE_NONE)) {
3226e0800cfSSumit Garg EMSG("bad parameters types: 0x%" PRIx32, types);
3236e0800cfSSumit Garg return TEE_ERROR_BAD_PARAMETERS;
3246e0800cfSSumit Garg }
3256e0800cfSSumit Garg
3266e0800cfSSumit Garg /* Output RNG rate (per second) */
3276e0800cfSSumit Garg params[0].value.a = 125;
3286e0800cfSSumit Garg
3296e0800cfSSumit Garg /*
3306e0800cfSSumit Garg * Quality/entropy per 1024 bit of output data. As we have used
3316e0800cfSSumit Garg * a vetted conditioner as per NIST.SP.800-90B to provide full
3326e0800cfSSumit Garg * entropy given our assumption of entropy estimate for raw sensor
3336e0800cfSSumit Garg * data.
3346e0800cfSSumit Garg */
3356e0800cfSSumit Garg params[0].value.b = 1024;
3366e0800cfSSumit Garg
3376e0800cfSSumit Garg return TEE_SUCCESS;
3386e0800cfSSumit Garg }
3396e0800cfSSumit Garg
invoke_command(void * pSessionContext __unused,uint32_t nCommandID,uint32_t nParamTypes,TEE_Param pParams[TEE_NUM_PARAMS])340a5d528c7SSumit Garg static TEE_Result invoke_command(void *pSessionContext __unused,
341a5d528c7SSumit Garg uint32_t nCommandID, uint32_t nParamTypes,
342a5d528c7SSumit Garg TEE_Param pParams[TEE_NUM_PARAMS])
343a5d528c7SSumit Garg {
344a5d528c7SSumit Garg FMSG("command entry point for pseudo-TA \"%s\"", PTA_NAME);
345a5d528c7SSumit Garg
346a5d528c7SSumit Garg switch (nCommandID) {
347a5d528c7SSumit Garg case PTA_CMD_GET_ENTROPY:
348a5d528c7SSumit Garg return rng_get_entropy(nParamTypes, pParams);
3496e0800cfSSumit Garg case PTA_CMD_GET_RNG_INFO:
3506e0800cfSSumit Garg return rng_get_info(nParamTypes, pParams);
351a5d528c7SSumit Garg default:
352a5d528c7SSumit Garg break;
353a5d528c7SSumit Garg }
354a5d528c7SSumit Garg
355a5d528c7SSumit Garg return TEE_ERROR_NOT_IMPLEMENTED;
356a5d528c7SSumit Garg }
357a5d528c7SSumit Garg
358a5d528c7SSumit Garg pseudo_ta_register(.uuid = PTA_RNG_UUID, .name = PTA_NAME,
359bbdbec2eSSumit Garg .flags = PTA_DEFAULT_FLAGS | TA_FLAG_DEVICE_ENUM,
360a5d528c7SSumit Garg .invoke_command_entry_point = invoke_command);
361