diff options
author | Marcel Selhorst <selhorst@crypto.rub.de> | 2005-07-27 14:45:12 -0400 |
---|---|---|
committer | Linus Torvalds <torvalds@g5.osdl.org> | 2005-07-27 19:26:06 -0400 |
commit | ebb81fdb3dd0be7514b84197c4f8388a17130f04 (patch) | |
tree | 8dd41338a44bb5194134828a8a14a36311193dac /drivers/char/tpm/tpm_infineon.c | |
parent | e77e17161ccb8bd877bf83b3611cd318e451c605 (diff) |
[PATCH] tpm: Support for Infineon TPM
This patch provides a new device driver for the Infineon SLD 9630 TT Trusted
Platform Module (TPM 1.1b) [1] which is embedded on Intel- mainboards or in
HP/ Fujitsu-Siemens / Toshiba-Notebooks. A nearly complete list where this
module is integrated in can be found in [2].
This kernel module acts as a communication gateway between the linux kernel
and the hardware chip and fits the TPM-specific interfaces created by IBM in
drivers/char/tpm/tpm.h
Further information about this module and a list of succesfully tested and
therefore supported hardware can be found at our project page [3].
[1]
http://www.infineon.com/cgi/ecrm.dll/ecrm/scripts/public_download.jsp?oid=114135&parent_oid=29049
[2]
http://www.tonymcfadden.net/tpmvendors.htm
[3]
http://www.prosec.rub.de/tpm
Signed-off-by: Marcel Selhorst <selhorst@crypto.rub.de>
Acked-by: Kylene Jo Hall <kjhall@us.ibm.com>
Signed-off-by: Andrew Morton <akpm@osdl.org>
Signed-off-by: Linus Torvalds <torvalds@osdl.org>
Diffstat (limited to 'drivers/char/tpm/tpm_infineon.c')
-rw-r--r-- | drivers/char/tpm/tpm_infineon.c | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/drivers/char/tpm/tpm_infineon.c b/drivers/char/tpm/tpm_infineon.c new file mode 100644 index 000000000000..07542f9e7644 --- /dev/null +++ b/drivers/char/tpm/tpm_infineon.c | |||
@@ -0,0 +1,467 @@ | |||
1 | /* | ||
2 | * Description: | ||
3 | * Device Driver for the Infineon Technologies | ||
4 | * SLD 9630 TT Trusted Platform Module | ||
5 | * Specifications at www.trustedcomputinggroup.org | ||
6 | * | ||
7 | * Copyright (C) 2005, Marcel Selhorst <selhorst@crypto.rub.de> | ||
8 | * Applied Data Security Group, Ruhr-University Bochum, Germany | ||
9 | * Project-Homepage: http://www.prosec.rub.de/tpm | ||
10 | * | ||
11 | * This program is free software; you can redistribute it and/or | ||
12 | * modify it under the terms of the GNU General Public License as | ||
13 | * published by the Free Software Foundation, version 2 of the | ||
14 | * License. | ||
15 | * | ||
16 | */ | ||
17 | |||
18 | #include "tpm.h" | ||
19 | |||
20 | /* Infineon specific definitions */ | ||
21 | /* maximum number of WTX-packages */ | ||
22 | #define TPM_MAX_WTX_PACKAGES 50 | ||
23 | /* msleep-Time for WTX-packages */ | ||
24 | #define TPM_WTX_MSLEEP_TIME 20 | ||
25 | /* msleep-Time --> Interval to check status register */ | ||
26 | #define TPM_MSLEEP_TIME 3 | ||
27 | /* gives number of max. msleep()-calls before throwing timeout */ | ||
28 | #define TPM_MAX_TRIES 5000 | ||
29 | #define TCPA_INFINEON_DEV_VEN_VALUE 0x15D1 | ||
30 | #define TPM_DATA (TPM_ADDR + 1) & 0xff | ||
31 | |||
32 | /* TPM header definitions */ | ||
33 | enum infineon_tpm_header { | ||
34 | TPM_VL_VER = 0x01, | ||
35 | TPM_VL_CHANNEL_CONTROL = 0x07, | ||
36 | TPM_VL_CHANNEL_PERSONALISATION = 0x0A, | ||
37 | TPM_VL_CHANNEL_TPM = 0x0B, | ||
38 | TPM_VL_CONTROL = 0x00, | ||
39 | TPM_INF_NAK = 0x15, | ||
40 | TPM_CTRL_WTX = 0x10, | ||
41 | TPM_CTRL_WTX_ABORT = 0x18, | ||
42 | TPM_CTRL_WTX_ABORT_ACK = 0x18, | ||
43 | TPM_CTRL_ERROR = 0x20, | ||
44 | TPM_CTRL_CHAININGACK = 0x40, | ||
45 | TPM_CTRL_CHAINING = 0x80, | ||
46 | TPM_CTRL_DATA = 0x04, | ||
47 | TPM_CTRL_DATA_CHA = 0x84, | ||
48 | TPM_CTRL_DATA_CHA_ACK = 0xC4 | ||
49 | }; | ||
50 | |||
51 | enum infineon_tpm_register { | ||
52 | WRFIFO = 0x00, | ||
53 | RDFIFO = 0x01, | ||
54 | STAT = 0x02, | ||
55 | CMD = 0x03 | ||
56 | }; | ||
57 | |||
58 | enum infineon_tpm_command_bits { | ||
59 | CMD_DIS = 0x00, | ||
60 | CMD_LP = 0x01, | ||
61 | CMD_RES = 0x02, | ||
62 | CMD_IRQC = 0x06 | ||
63 | }; | ||
64 | |||
65 | enum infineon_tpm_status_bits { | ||
66 | STAT_XFE = 0x00, | ||
67 | STAT_LPA = 0x01, | ||
68 | STAT_FOK = 0x02, | ||
69 | STAT_TOK = 0x03, | ||
70 | STAT_IRQA = 0x06, | ||
71 | STAT_RDA = 0x07 | ||
72 | }; | ||
73 | |||
74 | /* some outgoing values */ | ||
75 | enum infineon_tpm_values { | ||
76 | CHIP_ID1 = 0x20, | ||
77 | CHIP_ID2 = 0x21, | ||
78 | DAR = 0x30, | ||
79 | RESET_LP_IRQC_DISABLE = 0x41, | ||
80 | ENABLE_REGISTER_PAIR = 0x55, | ||
81 | IOLIMH = 0x60, | ||
82 | IOLIML = 0x61, | ||
83 | DISABLE_REGISTER_PAIR = 0xAA, | ||
84 | IDVENL = 0xF1, | ||
85 | IDVENH = 0xF2, | ||
86 | IDPDL = 0xF3, | ||
87 | IDPDH = 0xF4 | ||
88 | }; | ||
89 | |||
90 | static int number_of_wtx; | ||
91 | |||
92 | static int empty_fifo(struct tpm_chip *chip, int clear_wrfifo) | ||
93 | { | ||
94 | int status; | ||
95 | int check = 0; | ||
96 | int i; | ||
97 | |||
98 | if (clear_wrfifo) { | ||
99 | for (i = 0; i < 4096; i++) { | ||
100 | status = inb(chip->vendor->base + WRFIFO); | ||
101 | if (status == 0xff) { | ||
102 | if (check == 5) | ||
103 | break; | ||
104 | else | ||
105 | check++; | ||
106 | } | ||
107 | } | ||
108 | } | ||
109 | /* Note: The values which are currently in the FIFO of the TPM | ||
110 | are thrown away since there is no usage for them. Usually, | ||
111 | this has nothing to say, since the TPM will give its answer | ||
112 | immediately or will be aborted anyway, so the data here is | ||
113 | usually garbage and useless. | ||
114 | We have to clean this, because the next communication with | ||
115 | the TPM would be rubbish, if there is still some old data | ||
116 | in the Read FIFO. | ||
117 | */ | ||
118 | i = 0; | ||
119 | do { | ||
120 | status = inb(chip->vendor->base + RDFIFO); | ||
121 | status = inb(chip->vendor->base + STAT); | ||
122 | i++; | ||
123 | if (i == TPM_MAX_TRIES) | ||
124 | return -EIO; | ||
125 | } while ((status & (1 << STAT_RDA)) != 0); | ||
126 | return 0; | ||
127 | } | ||
128 | |||
129 | static int wait(struct tpm_chip *chip, int wait_for_bit) | ||
130 | { | ||
131 | int status; | ||
132 | int i; | ||
133 | for (i = 0; i < TPM_MAX_TRIES; i++) { | ||
134 | status = inb(chip->vendor->base + STAT); | ||
135 | /* check the status-register if wait_for_bit is set */ | ||
136 | if (status & 1 << wait_for_bit) | ||
137 | break; | ||
138 | msleep(TPM_MSLEEP_TIME); | ||
139 | } | ||
140 | if (i == TPM_MAX_TRIES) { /* timeout occurs */ | ||
141 | if (wait_for_bit == STAT_XFE) | ||
142 | dev_err(&chip->pci_dev->dev, | ||
143 | "Timeout in wait(STAT_XFE)\n"); | ||
144 | if (wait_for_bit == STAT_RDA) | ||
145 | dev_err(&chip->pci_dev->dev, | ||
146 | "Timeout in wait(STAT_RDA)\n"); | ||
147 | return -EIO; | ||
148 | } | ||
149 | return 0; | ||
150 | }; | ||
151 | |||
152 | static void wait_and_send(struct tpm_chip *chip, u8 sendbyte) | ||
153 | { | ||
154 | wait(chip, STAT_XFE); | ||
155 | outb(sendbyte, chip->vendor->base + WRFIFO); | ||
156 | } | ||
157 | |||
158 | /* Note: WTX means Waiting-Time-Extension. Whenever the TPM needs more | ||
159 | calculation time, it sends a WTX-package, which has to be acknowledged | ||
160 | or aborted. This usually occurs if you are hammering the TPM with key | ||
161 | creation. Set the maximum number of WTX-packages in the definitions | ||
162 | above, if the number is reached, the waiting-time will be denied | ||
163 | and the TPM command has to be resend. | ||
164 | */ | ||
165 | |||
166 | static void tpm_wtx(struct tpm_chip *chip) | ||
167 | { | ||
168 | number_of_wtx++; | ||
169 | dev_info(&chip->pci_dev->dev, "Granting WTX (%02d / %02d)\n", | ||
170 | number_of_wtx, TPM_MAX_WTX_PACKAGES); | ||
171 | wait_and_send(chip, TPM_VL_VER); | ||
172 | wait_and_send(chip, TPM_CTRL_WTX); | ||
173 | wait_and_send(chip, 0x00); | ||
174 | wait_and_send(chip, 0x00); | ||
175 | msleep(TPM_WTX_MSLEEP_TIME); | ||
176 | } | ||
177 | |||
178 | static void tpm_wtx_abort(struct tpm_chip *chip) | ||
179 | { | ||
180 | dev_info(&chip->pci_dev->dev, "Aborting WTX\n"); | ||
181 | wait_and_send(chip, TPM_VL_VER); | ||
182 | wait_and_send(chip, TPM_CTRL_WTX_ABORT); | ||
183 | wait_and_send(chip, 0x00); | ||
184 | wait_and_send(chip, 0x00); | ||
185 | number_of_wtx = 0; | ||
186 | msleep(TPM_WTX_MSLEEP_TIME); | ||
187 | } | ||
188 | |||
189 | static int tpm_inf_recv(struct tpm_chip *chip, u8 * buf, size_t count) | ||
190 | { | ||
191 | int i; | ||
192 | int ret; | ||
193 | u32 size = 0; | ||
194 | |||
195 | recv_begin: | ||
196 | /* start receiving header */ | ||
197 | for (i = 0; i < 4; i++) { | ||
198 | ret = wait(chip, STAT_RDA); | ||
199 | if (ret) | ||
200 | return -EIO; | ||
201 | buf[i] = inb(chip->vendor->base + RDFIFO); | ||
202 | } | ||
203 | |||
204 | if (buf[0] != TPM_VL_VER) { | ||
205 | dev_err(&chip->pci_dev->dev, | ||
206 | "Wrong transport protocol implementation!\n"); | ||
207 | return -EIO; | ||
208 | } | ||
209 | |||
210 | if (buf[1] == TPM_CTRL_DATA) { | ||
211 | /* size of the data received */ | ||
212 | size = ((buf[2] << 8) | buf[3]); | ||
213 | |||
214 | for (i = 0; i < size; i++) { | ||
215 | wait(chip, STAT_RDA); | ||
216 | buf[i] = inb(chip->vendor->base + RDFIFO); | ||
217 | } | ||
218 | |||
219 | if ((size == 0x6D00) && (buf[1] == 0x80)) { | ||
220 | dev_err(&chip->pci_dev->dev, | ||
221 | "Error handling on vendor layer!\n"); | ||
222 | return -EIO; | ||
223 | } | ||
224 | |||
225 | for (i = 0; i < size; i++) | ||
226 | buf[i] = buf[i + 6]; | ||
227 | |||
228 | size = size - 6; | ||
229 | return size; | ||
230 | } | ||
231 | |||
232 | if (buf[1] == TPM_CTRL_WTX) { | ||
233 | dev_info(&chip->pci_dev->dev, "WTX-package received\n"); | ||
234 | if (number_of_wtx < TPM_MAX_WTX_PACKAGES) { | ||
235 | tpm_wtx(chip); | ||
236 | goto recv_begin; | ||
237 | } else { | ||
238 | tpm_wtx_abort(chip); | ||
239 | goto recv_begin; | ||
240 | } | ||
241 | } | ||
242 | |||
243 | if (buf[1] == TPM_CTRL_WTX_ABORT_ACK) { | ||
244 | dev_info(&chip->pci_dev->dev, "WTX-abort acknowledged\n"); | ||
245 | return size; | ||
246 | } | ||
247 | |||
248 | if (buf[1] == TPM_CTRL_ERROR) { | ||
249 | dev_err(&chip->pci_dev->dev, "ERROR-package received:\n"); | ||
250 | if (buf[4] == TPM_INF_NAK) | ||
251 | dev_err(&chip->pci_dev->dev, | ||
252 | "-> Negative acknowledgement" | ||
253 | " - retransmit command!\n"); | ||
254 | return -EIO; | ||
255 | } | ||
256 | return -EIO; | ||
257 | } | ||
258 | |||
259 | static int tpm_inf_send(struct tpm_chip *chip, u8 * buf, size_t count) | ||
260 | { | ||
261 | int i; | ||
262 | int ret; | ||
263 | u8 count_high, count_low, count_4, count_3, count_2, count_1; | ||
264 | |||
265 | /* Disabling Reset, LP and IRQC */ | ||
266 | outb(RESET_LP_IRQC_DISABLE, chip->vendor->base + CMD); | ||
267 | |||
268 | ret = empty_fifo(chip, 1); | ||
269 | if (ret) { | ||
270 | dev_err(&chip->pci_dev->dev, "Timeout while clearing FIFO\n"); | ||
271 | return -EIO; | ||
272 | } | ||
273 | |||
274 | ret = wait(chip, STAT_XFE); | ||
275 | if (ret) | ||
276 | return -EIO; | ||
277 | |||
278 | count_4 = (count & 0xff000000) >> 24; | ||
279 | count_3 = (count & 0x00ff0000) >> 16; | ||
280 | count_2 = (count & 0x0000ff00) >> 8; | ||
281 | count_1 = (count & 0x000000ff); | ||
282 | count_high = ((count + 6) & 0xffffff00) >> 8; | ||
283 | count_low = ((count + 6) & 0x000000ff); | ||
284 | |||
285 | /* Sending Header */ | ||
286 | wait_and_send(chip, TPM_VL_VER); | ||
287 | wait_and_send(chip, TPM_CTRL_DATA); | ||
288 | wait_and_send(chip, count_high); | ||
289 | wait_and_send(chip, count_low); | ||
290 | |||
291 | /* Sending Data Header */ | ||
292 | wait_and_send(chip, TPM_VL_VER); | ||
293 | wait_and_send(chip, TPM_VL_CHANNEL_TPM); | ||
294 | wait_and_send(chip, count_4); | ||
295 | wait_and_send(chip, count_3); | ||
296 | wait_and_send(chip, count_2); | ||
297 | wait_and_send(chip, count_1); | ||
298 | |||
299 | /* Sending Data */ | ||
300 | for (i = 0; i < count; i++) { | ||
301 | wait_and_send(chip, buf[i]); | ||
302 | } | ||
303 | return count; | ||
304 | } | ||
305 | |||
306 | static void tpm_inf_cancel(struct tpm_chip *chip) | ||
307 | { | ||
308 | /* Nothing yet! | ||
309 | This has something to do with the internal functions | ||
310 | of the TPM. Abort isn't really necessary... | ||
311 | */ | ||
312 | } | ||
313 | |||
314 | static DEVICE_ATTR(pubek, S_IRUGO, tpm_show_pubek, NULL); | ||
315 | static DEVICE_ATTR(pcrs, S_IRUGO, tpm_show_pcrs, NULL); | ||
316 | static DEVICE_ATTR(caps, S_IRUGO, tpm_show_caps, NULL); | ||
317 | static DEVICE_ATTR(cancel, S_IWUSR | S_IWGRP, NULL, tpm_store_cancel); | ||
318 | |||
319 | static struct attribute *inf_attrs[] = { | ||
320 | &dev_attr_pubek.attr, | ||
321 | &dev_attr_pcrs.attr, | ||
322 | &dev_attr_caps.attr, | ||
323 | &dev_attr_cancel.attr, | ||
324 | NULL, | ||
325 | }; | ||
326 | |||
327 | static struct attribute_group inf_attr_grp = {.attrs = inf_attrs }; | ||
328 | |||
329 | static struct file_operations inf_ops = { | ||
330 | .owner = THIS_MODULE, | ||
331 | .llseek = no_llseek, | ||
332 | .open = tpm_open, | ||
333 | .read = tpm_read, | ||
334 | .write = tpm_write, | ||
335 | .release = tpm_release, | ||
336 | }; | ||
337 | |||
338 | static struct tpm_vendor_specific tpm_inf = { | ||
339 | .recv = tpm_inf_recv, | ||
340 | .send = tpm_inf_send, | ||
341 | .cancel = tpm_inf_cancel, | ||
342 | .req_complete_mask = 0, | ||
343 | .req_complete_val = 0, | ||
344 | .attr_group = &inf_attr_grp, | ||
345 | .miscdev = {.fops = &inf_ops,}, | ||
346 | }; | ||
347 | |||
348 | static int __devinit tpm_inf_probe(struct pci_dev *pci_dev, | ||
349 | const struct pci_device_id *pci_id) | ||
350 | { | ||
351 | int rc = 0; | ||
352 | u8 iol, ioh; | ||
353 | int vendorid[2]; | ||
354 | int version[2]; | ||
355 | int productid[2]; | ||
356 | |||
357 | if (pci_enable_device(pci_dev)) | ||
358 | return -EIO; | ||
359 | |||
360 | dev_info(&pci_dev->dev, "LPC-bus found at 0x%x\n", pci_id->device); | ||
361 | |||
362 | /* query chip for its vendor, its version number a.s.o. */ | ||
363 | outb(ENABLE_REGISTER_PAIR, TPM_ADDR); | ||
364 | outb(IDVENL, TPM_ADDR); | ||
365 | vendorid[1] = inb(TPM_DATA); | ||
366 | outb(IDVENH, TPM_ADDR); | ||
367 | vendorid[0] = inb(TPM_DATA); | ||
368 | outb(IDPDL, TPM_ADDR); | ||
369 | productid[1] = inb(TPM_DATA); | ||
370 | outb(IDPDH, TPM_ADDR); | ||
371 | productid[0] = inb(TPM_DATA); | ||
372 | outb(CHIP_ID1, TPM_ADDR); | ||
373 | version[1] = inb(TPM_DATA); | ||
374 | outb(CHIP_ID2, TPM_ADDR); | ||
375 | version[0] = inb(TPM_DATA); | ||
376 | |||
377 | if ((vendorid[0] << 8 | vendorid[1]) == (TCPA_INFINEON_DEV_VEN_VALUE)) { | ||
378 | |||
379 | /* read IO-ports from TPM */ | ||
380 | outb(IOLIMH, TPM_ADDR); | ||
381 | ioh = inb(TPM_DATA); | ||
382 | outb(IOLIML, TPM_ADDR); | ||
383 | iol = inb(TPM_DATA); | ||
384 | tpm_inf.base = (ioh << 8) | iol; | ||
385 | |||
386 | if (tpm_inf.base == 0) { | ||
387 | dev_err(&pci_dev->dev, "No IO-ports set!\n"); | ||
388 | pci_disable_device(pci_dev); | ||
389 | return -ENODEV; | ||
390 | } | ||
391 | |||
392 | /* activate register */ | ||
393 | outb(DAR, TPM_ADDR); | ||
394 | outb(0x01, TPM_DATA); | ||
395 | outb(DISABLE_REGISTER_PAIR, TPM_ADDR); | ||
396 | |||
397 | /* disable RESET, LP and IRQC */ | ||
398 | outb(RESET_LP_IRQC_DISABLE, tpm_inf.base + CMD); | ||
399 | |||
400 | /* Finally, we're done, print some infos */ | ||
401 | dev_info(&pci_dev->dev, "TPM found: " | ||
402 | "io base 0x%x, " | ||
403 | "chip version %02x%02x, " | ||
404 | "vendor id %x%x (Infineon), " | ||
405 | "product id %02x%02x" | ||
406 | "%s\n", | ||
407 | tpm_inf.base, | ||
408 | version[0], version[1], | ||
409 | vendorid[0], vendorid[1], | ||
410 | productid[0], productid[1], ((productid[0] == 0) | ||
411 | && (productid[1] == | ||
412 | 6)) ? | ||
413 | " (SLD 9630 TT 1.1)" : ""); | ||
414 | |||
415 | rc = tpm_register_hardware(pci_dev, &tpm_inf); | ||
416 | if (rc < 0) { | ||
417 | pci_disable_device(pci_dev); | ||
418 | return -ENODEV; | ||
419 | } | ||
420 | return 0; | ||
421 | } else { | ||
422 | dev_info(&pci_dev->dev, "No Infineon TPM found!\n"); | ||
423 | pci_disable_device(pci_dev); | ||
424 | return -ENODEV; | ||
425 | } | ||
426 | } | ||
427 | |||
428 | static struct pci_device_id tpm_pci_tbl[] __devinitdata = { | ||
429 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82801BA_0)}, | ||
430 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82801CA_12)}, | ||
431 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82801DB_0)}, | ||
432 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82801DB_12)}, | ||
433 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82801EB_0)}, | ||
434 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH6_0)}, | ||
435 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH6_1)}, | ||
436 | {PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH6_2)}, | ||
437 | {0,} | ||
438 | }; | ||
439 | |||
440 | MODULE_DEVICE_TABLE(pci, tpm_pci_tbl); | ||
441 | |||
442 | static struct pci_driver inf_pci_driver = { | ||
443 | .name = "tpm_inf", | ||
444 | .id_table = tpm_pci_tbl, | ||
445 | .probe = tpm_inf_probe, | ||
446 | .remove = __devexit_p(tpm_remove), | ||
447 | .suspend = tpm_pm_suspend, | ||
448 | .resume = tpm_pm_resume, | ||
449 | }; | ||
450 | |||
451 | static int __init init_inf(void) | ||
452 | { | ||
453 | return pci_register_driver(&inf_pci_driver); | ||
454 | } | ||
455 | |||
456 | static void __exit cleanup_inf(void) | ||
457 | { | ||
458 | pci_unregister_driver(&inf_pci_driver); | ||
459 | } | ||
460 | |||
461 | module_init(init_inf); | ||
462 | module_exit(cleanup_inf); | ||
463 | |||
464 | MODULE_AUTHOR("Marcel Selhorst <selhorst@crypto.rub.de>"); | ||
465 | MODULE_DESCRIPTION("Driver for Infineon TPM SLD 9630 TT"); | ||
466 | MODULE_VERSION("1.4"); | ||
467 | MODULE_LICENSE("GPL"); | ||