All of lore.kernel.org
 help / color / mirror / Atom feed
From: Zhuoying Cai <zycai@linux.ibm.com>
To: qemu-s390x@nongnu.org, qemu-devel@nongnu.org
Cc: jrossi@linux.ibm.com, cohuck@redhat.com, berrange@redhat.com,
	richard.henderson@linaro.org, david@kernel.org,
	walling@linux.ibm.com, jjherne@linux.ibm.com,
	pasic@linux.ibm.com, borntraeger@linux.ibm.com,
	farman@linux.ibm.com, mjrosato@linux.ibm.com, iii@linux.ibm.com,
	eblake@redhat.com, armbru@redhat.com, zycai@linux.ibm.com,
	alifm@linux.ibm.com, brueckner@linux.ibm.com,
	pierrick.bouvier@oss.qualcomm.com, jdaley@linux.ibm.com
Subject: [PATCH v13 04/33] hw/s390x/ipl: Create certificate store
Date: Thu,  2 Jul 2026 22:29:02 -0400	[thread overview]
Message-ID: <20260703022933.1805010-5-zycai@linux.ibm.com> (raw)
In-Reply-To: <20260703022933.1805010-1-zycai@linux.ibm.com>

Create a certificate store for boot certificates used for secure IPL.

Load certificates from the `boot-certs` parameter of s390-ccw-virtio
machine type option into the cert store.

Currently, only X.509 certificates in PEM format are supported, as the
QEMU command line accepts certificates in PEM format only.

The raw Base64 data is stored, as well as the certificate's size.  The
binary (DER) size is stored as well, which may later be utilized for
secure boot (signature verification).

Signed-off-by: Zhuoying Cai <zycai@linux.ibm.com>
Reviewed-by: Farhan Ali<alifm@linux.ibm.com>
---
 docs/specs/index.rst            |   1 +
 docs/specs/s390x-secure-ipl.rst |  20 +++
 hw/s390x/cert-store.c           | 238 ++++++++++++++++++++++++++++++++
 hw/s390x/cert-store.h           |  39 ++++++
 hw/s390x/ipl.c                  |  10 ++
 hw/s390x/ipl.h                  |   3 +
 hw/s390x/meson.build            |   1 +
 include/hw/s390x/ipl/qipl.h     |   2 +
 8 files changed, 314 insertions(+)
 create mode 100644 docs/specs/s390x-secure-ipl.rst
 create mode 100644 hw/s390x/cert-store.c
 create mode 100644 hw/s390x/cert-store.h

diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index b7909a108a..76d439782c 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -40,3 +40,4 @@ guest hardware that is specific to QEMU.
    riscv-aia
    aspeed-intc
    iommu-testdev
+   s390x-secure-ipl
diff --git a/docs/specs/s390x-secure-ipl.rst b/docs/specs/s390x-secure-ipl.rst
new file mode 100644
index 0000000000..d7c0d4eaac
--- /dev/null
+++ b/docs/specs/s390x-secure-ipl.rst
@@ -0,0 +1,20 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+s390 Certificate Store and Functions
+------------------------------------
+
+s390 Certificate Store
+^^^^^^^^^^^^^^^^^^^^^^
+
+A certificate store is implemented for s390-ccw guests to retain within
+memory all certificates provided by the user via the command-line, which
+are expected to be stored somewhere on the host's file system. The store
+will keep track of the number of certificates, their respective size,
+and a summation of the sizes.
+
+Each certificate is stroed in an S390IPLCertificate struct, which has a
+name (converted to EBCDIC), size fields of PEM and DER data, and the raw
+PEM Base64 data.
+
+Note: A maximum of 64 certificates are allowed to be stored in the certificate
+store.
diff --git a/hw/s390x/cert-store.c b/hw/s390x/cert-store.c
new file mode 100644
index 0000000000..aa2c1259eb
--- /dev/null
+++ b/hw/s390x/cert-store.c
@@ -0,0 +1,238 @@
+/*
+ * S390 certificate store implementation
+ *
+ * Copyright 2025 IBM Corp.
+ * Author(s): Zhuoying Cai <zycai@linux.ibm.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "cert-store.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "qemu/option.h"
+#include "qemu/config-file.h"
+#include "hw/s390x/ebcdic.h"
+#include "hw/s390x/s390-virtio-ccw.h"
+#include "qemu/cutils.h"
+#include "crypto/x509-utils.h"
+#include "qapi/qapi-types-machine-s390x.h"
+
+static BootCertificatesList *s390_get_boot_certs(void)
+{
+    return S390_CCW_MACHINE(qdev_get_machine())->boot_certs;
+}
+
+static S390IPLCertificate *init_cert(char *path, Error **errp)
+{
+    int rc;
+    size_t size;
+    size_t der_len;
+    char name[CERT_NAME_MAX_LEN];
+    g_autofree char *buf = NULL;
+    g_autofree gchar *filename = NULL;
+    S390IPLCertificate *cert = NULL;
+    g_autofree uint8_t *cert_der = NULL;
+    Error *local_err = NULL;
+
+    filename = g_path_get_basename(path);
+
+    if (!g_file_get_contents(path, &buf, &size, NULL)) {
+        error_setg(errp, "Failed to load certificate: %s", path);
+        return NULL;
+    }
+
+    rc = qcrypto_x509_convert_cert_der((uint8_t *)buf, size,
+                                       &cert_der, &der_len, &local_err);
+    if (rc != 0) {
+        error_propagate_prepend(errp, local_err,
+                                "Failed to initialize certificate: %s: ", path);
+        return NULL;
+    }
+
+    cert = g_new0(S390IPLCertificate, 1);
+    cert->size = size;
+    /*
+     * Store DER length only - reused for size calculation.
+     * cert_der is discarded because DER certificate data will be used once
+     * and can be regenerated from cert->raw.
+     */
+    cert->der_size = der_len;
+    /* store raw pointer - ownership transfers to cert */
+    cert->raw = (uint8_t *)g_steal_pointer(&buf);
+
+    /*
+     * Left justified certificate name with padding on the right with blanks.
+     * Convert certificate name to EBCDIC.
+     */
+    strpadcpy(name, CERT_NAME_MAX_LEN, filename, ' ');
+    ebcdic_put(cert->name, name, CERT_NAME_MAX_LEN);
+
+    return cert;
+}
+
+static int update_cert_store(S390IPLCertificateStore *cert_store,
+                             S390IPLCertificate *cert)
+{
+    size_t data_buf_size;
+    size_t keyid_buf_size;
+    size_t hash_buf_size;
+    size_t cert_buf_size;
+
+    /* length field is word aligned for later DIAG use */
+    keyid_buf_size = ROUND_UP(CERT_KEY_ID_LEN, 4);
+    hash_buf_size = ROUND_UP(CERT_HASH_LEN, 4);
+    cert_buf_size = ROUND_UP(cert->der_size, 4);
+    data_buf_size = keyid_buf_size + hash_buf_size + cert_buf_size;
+
+    if (cert_store->largest_cert_size < data_buf_size) {
+        cert_store->largest_cert_size = data_buf_size;
+    }
+
+    if (cert_store->count >= MAX_CERTIFICATES) {
+        error_report("Cert store is full");
+        return -1;
+    }
+
+    cert_store->certs[cert_store->count] = *cert;
+    cert_store->total_bytes += data_buf_size;
+    cert_store->count++;
+
+    return 0;
+}
+
+static GPtrArray *get_cert_paths(Error **errp)
+{
+    struct stat st;
+    BootCertificatesList *path_list = NULL;
+    BootCertificatesList *list = NULL;
+    gchar *cert_path;
+    GDir *dir = NULL;
+    const gchar *filename;
+    bool is_empty;
+    g_autoptr(GError) err = NULL;
+    g_autoptr(GPtrArray) cert_path_builder = g_ptr_array_new_full(0, g_free);
+
+    path_list = s390_get_boot_certs();
+
+    for (list = path_list; list; list = list->next) {
+        cert_path = list->value->path;
+
+        if (g_strcmp0(cert_path, "") == 0) {
+            error_setg(errp, "Empty path in certificate path list is not allowed");
+            goto fail;
+        }
+
+        if (stat(cert_path, &st) != 0) {
+            error_setg(errp, "Failed to stat path '%s': %s",
+                       cert_path, g_strerror(errno));
+            goto fail;
+        }
+
+        if (S_ISREG(st.st_mode)) {
+            if (!g_str_has_suffix(cert_path, ".pem")) {
+                error_setg(errp, "Certificate file '%s' must have a .pem extension",
+                           cert_path);
+                goto fail;
+            }
+
+            g_ptr_array_add(cert_path_builder, g_strdup(cert_path));
+        } else if (S_ISDIR(st.st_mode)) {
+            dir = g_dir_open(cert_path, 0, &err);
+            if (dir == NULL) {
+                error_setg(errp, "Failed to open directory '%s': %s",
+                           cert_path, err->message);
+
+                goto fail;
+            }
+
+            is_empty = true;
+            while ((filename = g_dir_read_name(dir))) {
+                is_empty = false;
+
+                if (g_str_has_suffix(filename, ".pem")) {
+                    g_ptr_array_add(cert_path_builder,
+                                    g_build_filename(cert_path, filename, NULL));
+                } else {
+                    warn_report("skipping '%s': not a .pem file", filename);
+                }
+            }
+
+            if (is_empty) {
+                warn_report("'%s' directory is empty", cert_path);
+            }
+
+            g_dir_close(dir);
+        } else {
+            error_setg(errp, "Path '%s' is neither a file nor a directory", cert_path);
+            goto fail;
+        }
+    }
+
+    qapi_free_BootCertificatesList(path_list);
+    return g_steal_pointer(&cert_path_builder);
+
+fail:
+    qapi_free_BootCertificatesList(path_list);
+    return NULL;
+}
+
+static void s390_ipl_destroy_cert_store(S390IPLCertificateStore *cert_store)
+{
+    for (int i = 0; i < cert_store->count; i++) {
+        g_free(cert_store->certs[i].raw);
+    }
+    memset(cert_store, 0, sizeof(*cert_store));
+}
+
+void s390_ipl_create_cert_store(S390IPLCertificateStore *cert_store)
+{
+    GPtrArray *cert_path_builder;
+    Error *err = NULL;
+
+    /* If cert store is already populated, then no work to do */
+    if (cert_store->count) {
+        return;
+    }
+
+    cert_path_builder = get_cert_paths(&err);
+    if (cert_path_builder == NULL) {
+        error_report_err(err);
+        exit(1);
+    }
+
+    if (cert_path_builder->len == 0) {
+        g_ptr_array_free(cert_path_builder, TRUE);
+        return;
+    }
+
+    if (cert_path_builder->len > MAX_CERTIFICATES) {
+        error_report("Cert store exceeds maximum of %d certificates", MAX_CERTIFICATES);
+        g_ptr_array_free(cert_path_builder, TRUE);
+        exit(1);
+    }
+
+    cert_store->largest_cert_size = 0;
+    cert_store->total_bytes = 0;
+
+    for (int i = 0; i < cert_path_builder->len; i++) {
+        g_autofree S390IPLCertificate *cert =
+                    init_cert((char *) cert_path_builder->pdata[i],
+                              &err);
+        if (!cert) {
+            error_report_err(err);
+            g_ptr_array_free(cert_path_builder, TRUE);
+            s390_ipl_destroy_cert_store(cert_store);
+            exit(1);
+        }
+
+        if (update_cert_store(cert_store, cert)) {
+            g_ptr_array_free(cert_path_builder, TRUE);
+            s390_ipl_destroy_cert_store(cert_store);
+            exit(1);
+        }
+    }
+
+    g_ptr_array_free(cert_path_builder, TRUE);
+}
diff --git a/hw/s390x/cert-store.h b/hw/s390x/cert-store.h
new file mode 100644
index 0000000000..7fc9503cb9
--- /dev/null
+++ b/hw/s390x/cert-store.h
@@ -0,0 +1,39 @@
+/*
+ * S390 certificate store
+ *
+ * Copyright 2025 IBM Corp.
+ * Author(s): Zhuoying Cai <zycai@linux.ibm.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_S390_CERT_STORE_H
+#define HW_S390_CERT_STORE_H
+
+#include "hw/s390x/ipl/qipl.h"
+#include "crypto/x509-utils.h"
+
+#define CERT_NAME_MAX_LEN  64
+
+#define CERT_KEY_ID_LEN    QCRYPTO_HASH_DIGEST_LEN_SHA256
+#define CERT_HASH_LEN      QCRYPTO_HASH_DIGEST_LEN_SHA256
+
+struct S390IPLCertificate {
+    uint8_t name[CERT_NAME_MAX_LEN];
+    size_t  size;
+    size_t  der_size;
+    uint8_t *raw;
+};
+typedef struct S390IPLCertificate S390IPLCertificate;
+
+struct S390IPLCertificateStore {
+    uint16_t count;
+    size_t   largest_cert_size;
+    size_t   total_bytes;
+    S390IPLCertificate certs[MAX_CERTIFICATES];
+};
+typedef struct S390IPLCertificateStore S390IPLCertificateStore;
+
+void s390_ipl_create_cert_store(S390IPLCertificateStore *cert_store);
+
+#endif
diff --git a/hw/s390x/ipl.c b/hw/s390x/ipl.c
index 4cca21c621..09c24203c7 100644
--- a/hw/s390x/ipl.c
+++ b/hw/s390x/ipl.c
@@ -38,6 +38,7 @@
 #include "qemu/option.h"
 #include "qemu/ctype.h"
 #include "standard-headers/linux/virtio_ids.h"
+#include "cert-store.h"
 
 #define KERN_IMAGE_START                0x010000UL
 #define LINUX_MAGIC_ADDR                0x010008UL
@@ -453,6 +454,13 @@ void s390_ipl_convert_loadparm(char *ascii_lp, uint8_t *ebcdic_lp)
     }
 }
 
+S390IPLCertificateStore *s390_ipl_get_certificate_store(void)
+{
+    S390IPLState *ipl = get_ipl_device();
+
+    return &ipl->cert_store;
+}
+
 static bool s390_build_iplb(DeviceState *dev_st, IplParameterBlock *iplb)
 {
     CcwDevice *ccw_dev = NULL;
@@ -767,6 +775,8 @@ void s390_ipl_prepare_cpu(S390CPU *cpu)
     cpu->env.psw.addr = ipl->start_addr;
     cpu->env.psw.mask = IPL_PSW_MASK;
 
+    s390_ipl_create_cert_store(&ipl->cert_store);
+
     if (!ipl->kernel || ipl->iplb_valid) {
         cpu->env.psw.addr = ipl->bios_start_addr;
         if (!ipl->iplb_valid) {
diff --git a/hw/s390x/ipl.h b/hw/s390x/ipl.h
index fac30763df..f5a49a4431 100644
--- a/hw/s390x/ipl.h
+++ b/hw/s390x/ipl.h
@@ -13,6 +13,7 @@
 #ifndef HW_S390_IPL_H
 #define HW_S390_IPL_H
 
+#include "cert-store.h"
 #include "target/s390x/cpu.h"
 #include "exec/target_page.h"
 #include "system/address-spaces.h"
@@ -35,6 +36,7 @@ int s390_ipl_pv_unpack(struct S390PVResponse *pv_resp);
 void s390_ipl_prepare_cpu(S390CPU *cpu);
 IplParameterBlock *s390_ipl_get_iplb(void);
 IplParameterBlock *s390_ipl_get_iplb_pv(void);
+S390IPLCertificateStore *s390_ipl_get_certificate_store(void);
 
 enum s390_reset {
     /* default is a reset not triggered by a CPU e.g. issued by QMP */
@@ -63,6 +65,7 @@ struct S390IPLState {
     IplParameterBlock iplb;
     IplParameterBlock iplb_pv;
     QemuIplParameters qipl;
+    S390IPLCertificateStore cert_store;
     uint64_t start_addr;
     uint64_t compat_start_addr;
     uint64_t bios_start_addr;
diff --git a/hw/s390x/meson.build b/hw/s390x/meson.build
index 57cc2a6be3..6b39ad012f 100644
--- a/hw/s390x/meson.build
+++ b/hw/s390x/meson.build
@@ -17,6 +17,7 @@ s390x_ss.add(files(
   'sclpcpu.c',
   'sclpquiesce.c',
   'tod.c',
+  'cert-store.c',
 ))
 s390x_ss.add(when: 'CONFIG_KVM', if_true: files(
   'tod-kvm.c',
diff --git a/include/hw/s390x/ipl/qipl.h b/include/hw/s390x/ipl/qipl.h
index 8d3c83a80b..ed1a91182a 100644
--- a/include/hw/s390x/ipl/qipl.h
+++ b/include/hw/s390x/ipl/qipl.h
@@ -31,6 +31,8 @@ typedef enum S390IplType S390IplType;
 
 #define QEMU_DEFAULT_IPL S390_IPL_TYPE_CCW
 
+#define MAX_CERTIFICATES  64
+
 /*
  * The QEMU IPL Parameters will be stored at absolute address
  * 204 (0xcc) which means it is 32-bit word aligned but not
-- 
2.54.0



  parent reply	other threads:[~2026-07-03  2:32 UTC|newest]

Thread overview: 45+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-03  2:28 [PATCH v13 00/33] Secure IPL Support for SCSI Scheme of virtio-blk/virtio-scsi Devices Zhuoying Cai
2026-07-03  2:28 ` [PATCH v13 01/33] Add boot-certs to s390-ccw-virtio machine type option Zhuoying Cai
2026-07-03 13:24   ` Matthew Rosato
2026-07-03  2:29 ` [PATCH v13 02/33] crypto/x509-utils: Refactor with GNUTLS fallback Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 03/33] crypto/x509-utils: Add helper functions for certificate store Zhuoying Cai
2026-07-03  2:29 ` Zhuoying Cai [this message]
2026-07-03 16:56   ` [PATCH v13 04/33] hw/s390x/ipl: Create " Matthew Rosato
2026-07-03  2:29 ` [PATCH v13 05/33] s390x/diag: Introduce DIAG 320 for Certificate Store Facility Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 06/33] s390x/diag: Refactor address validation check from diag308_parm_check Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 07/33] s390x/diag: Implement DIAG 320 subcode 1 Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 08/33] crypto/x509-utils: Add helper functions for DIAG 320 subcode 2 Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 09/33] s390x/diag: Implement " Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 10/33] hw/s390x: Define finite size for single entry VCEntry Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 11/33] s390x/diag: Introduce DIAG 508 for secure IPL operations Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 12/33] crypto/x509-utils: Add helper functions for DIAG 508 subcode 1 Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 13/33] s390x/diag: Generalize s390_ipl_read/write to accept void * Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 14/33] s390x/diag: Implement DIAG 508 subcode 1 for signature verification Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 15/33] s390x/ipl: Introduce IPL Information Report Block (IIRB) Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 16/33] pc-bios/s390-ccw: Define memory for IPLB and convert IPLB to pointers Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 17/33] hw/s390x/ipl: Add IPIB flags to IPL Parameter Block Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 18/33] hw/s390x/ipl: Rework s390_ipl_map_iplb_chain for certificate storage Zhuoying Cai
2026-07-03 13:47   ` Eric Farman
2026-07-03  2:29 ` [PATCH v13 19/33] s390x: Guest support for Secure-IPL Facility Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 20/33] pc-bios/s390-ccw: Refactor zipl_run() Zhuoying Cai
2026-07-03  2:40   ` Philippe Mathieu-Daudé
2026-07-03  2:29 ` [PATCH v13 21/33] pc-bios/s390-ccw: Rework zipl_load_segment function Zhuoying Cai
2026-07-03  2:38   ` Philippe Mathieu-Daudé
2026-07-03 14:28     ` Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 22/33] pc-bios/s390-ccw: Introduce ZiplBootMode enum for IPL mode selection Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 23/33] pc-bios/s390-ccw: Add signature verification for secure IPL in audit mode Zhuoying Cai
2026-07-03 11:40   ` Eric Farman
2026-07-03 13:56     ` Zhuoying Cai
2026-07-04  0:43       ` Eric Farman
2026-07-03 13:58     ` Eric Farman
2026-07-03  2:29 ` [PATCH v13 24/33] pc-bios/s390-ccw: Add signed component address overlap checks Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 25/33] s390x: Guest support for Secure-IPL Code Loading Attributes Facility (SCLAF) Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 26/33] pc-bios/s390-ccw: Add additional security checks for secure boot Zhuoying Cai
2026-07-03 12:17   ` Eric Farman
2026-07-03  2:29 ` [PATCH v13 27/33] Add secure-boot to s390-ccw-virtio machine type option Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 28/33] hw/s390x/ipl: Set IPIB flags for secure IPL Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 29/33] pc-bios/s390-ccw: Handle true secure IPL mode Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 30/33] hw/s390x/ipl: Handle secure boot with multiple boot devices Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 31/33] tests/functional/s390x: Add secure IPL functional test Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 32/33] docs/specs: Add secure IPL documentation Zhuoying Cai
2026-07-03  2:29 ` [PATCH v13 33/33] docs/system/s390x: " Zhuoying Cai

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260703022933.1805010-5-zycai@linux.ibm.com \
    --to=zycai@linux.ibm.com \
    --cc=alifm@linux.ibm.com \
    --cc=armbru@redhat.com \
    --cc=berrange@redhat.com \
    --cc=borntraeger@linux.ibm.com \
    --cc=brueckner@linux.ibm.com \
    --cc=cohuck@redhat.com \
    --cc=david@kernel.org \
    --cc=eblake@redhat.com \
    --cc=farman@linux.ibm.com \
    --cc=iii@linux.ibm.com \
    --cc=jdaley@linux.ibm.com \
    --cc=jjherne@linux.ibm.com \
    --cc=jrossi@linux.ibm.com \
    --cc=mjrosato@linux.ibm.com \
    --cc=pasic@linux.ibm.com \
    --cc=pierrick.bouvier@oss.qualcomm.com \
    --cc=qemu-devel@nongnu.org \
    --cc=qemu-s390x@nongnu.org \
    --cc=richard.henderson@linaro.org \
    --cc=walling@linux.ibm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.