All of lore.kernel.org
 help / color / mirror / Atom feed
From: AKASHI Takahiro <takahiro.akashi@linaro.org>
To: Heinrich Schuchardt <xypron.glpk@gmx.de>
Cc: sjg@chromium.org, ilias.apalodimas@linaro.org,
	sughosh.ganu@linaro.org, masami.hiramatsu@linaro.org,
	mark.kettenis@xs4all.nl, u-boot@lists.denx.de
Subject: Re: [PATCH v11 5/9] test/py: efi_capsule: add image authentication test
Date: Mon, 14 Feb 2022 09:43:06 +0900	[thread overview]
Message-ID: <20220214004306.GA39639@laputa> (raw)
In-Reply-To: <fc6a35fe-7034-585c-866c-b1c01e5d3d10@gmx.de>

Heinrich,

On Fri, Feb 11, 2022 at 08:25:15PM +0100, Heinrich Schuchardt wrote:
> On 2/9/22 11:10, AKASHI Takahiro wrote:
> > Add a couple of test cases against capsule image authentication
> > for capsule-on-disk, where only a signed capsule file with the verified
> > signature will be applied to the system.
> > 
> > Due to the difficulty of embedding a public key (esl file) in U-Boot
> > binary during pytest setup time, all the keys/certificates are pre-created.
> > 
> > Signed-off-by: AKASHI Takahiro <takahiro.akashi@linaro.org>
> > Reviewed-by: Simon Glass <sjg@chromium.org>
> > Acked-by: Ilias Apalodimas <ilias.apalodimas@linaro.org>
> 
> The test is not executed on Gitlab:
> 
> test/py/tests/test_efi_capsule/test_capsule_firmware_signed.py sss
> 
> SKIPPED [3] /builds/u-boot/custodians/u-boot-efi/test/py/conftest.py:490:
> .config feature "efi_capsule_authenticate" not enabled
> 
> Please, provide a defconfig with CONFIG_EFI_CAPSULE_AUTHENTICATE=y in a
> follow-up patch.

This is somehow intentional.
I don't remember quite well, but when I tried to add another defconfig file
for sandbox to initiate some test in the past, you or Simon (sorry if I
remember incorrectly here) opposed it.

Please also note that adding CONFIG_EFI_CAPSULE_AUTHENTICATE to
sandbox_defconfig doesn't make sense as it makes non-signed capsule
tests (test_capsule_firmware.py) meaningless.

-Takahiro Akashi

> Best regards
> 
> Heinrich
> 
> 
> > ---
> >   .../py/tests/test_efi_capsule/capsule_defs.py |   5 +
> >   test/py/tests/test_efi_capsule/conftest.py    |  52 +++-
> >   test/py/tests/test_efi_capsule/signature.dts  |  10 +
> >   .../test_capsule_firmware_signed.py           | 254 ++++++++++++++++++
> >   4 files changed, 318 insertions(+), 3 deletions(-)
> >   create mode 100644 test/py/tests/test_efi_capsule/signature.dts
> >   create mode 100644 test/py/tests/test_efi_capsule/test_capsule_firmware_signed.py
> > 
> > diff --git a/test/py/tests/test_efi_capsule/capsule_defs.py b/test/py/tests/test_efi_capsule/capsule_defs.py
> > index 4fd6353c2040..59b40f11bd1d 100644
> > --- a/test/py/tests/test_efi_capsule/capsule_defs.py
> > +++ b/test/py/tests/test_efi_capsule/capsule_defs.py
> > @@ -3,3 +3,8 @@
> >   # Directories
> >   CAPSULE_DATA_DIR = '/EFI/CapsuleTestData'
> >   CAPSULE_INSTALL_DIR = '/EFI/UpdateCapsule'
> > +
> > +# v1.5.1 or earlier of efitools has a bug in sha256 calculation, and
> > +# you need build a newer version on your own.
> > +# The path must terminate with '/' if it is not null.
> > +EFITOOLS_PATH = ''
> > diff --git a/test/py/tests/test_efi_capsule/conftest.py b/test/py/tests/test_efi_capsule/conftest.py
> > index 6ad5608cd71c..27c05971ca32 100644
> > --- a/test/py/tests/test_efi_capsule/conftest.py
> > +++ b/test/py/tests/test_efi_capsule/conftest.py
> > @@ -10,13 +10,13 @@ import pytest
> >   from capsule_defs import *
> > 
> >   #
> > -# Fixture for UEFI secure boot test
> > +# Fixture for UEFI capsule test
> >   #
> > 
> > -
> >   @pytest.fixture(scope='session')
> >   def efi_capsule_data(request, u_boot_config):
> > -    """Set up a file system to be used in UEFI capsule test.
> > +    """Set up a file system to be used in UEFI capsule and
> > +       authentication test.
> > 
> >       Args:
> >           request: Pytest request object.
> > @@ -40,6 +40,36 @@ def efi_capsule_data(request, u_boot_config):
> >           check_call('mkdir -p %s' % data_dir, shell=True)
> >           check_call('mkdir -p %s' % install_dir, shell=True)
> > 
> > +        capsule_auth_enabled = u_boot_config.buildconfig.get(
> > +                    'config_efi_capsule_authenticate')
> > +        if capsule_auth_enabled:
> > +            # Create private key (SIGNER.key) and certificate (SIGNER.crt)
> > +            check_call('cd %s; '
> > +                       'openssl req -x509 -sha256 -newkey rsa:2048 '
> > +                            '-subj /CN=TEST_SIGNER/ -keyout SIGNER.key '
> > +                            '-out SIGNER.crt -nodes -days 365'
> > +                       % data_dir, shell=True)
> > +            check_call('cd %s; %scert-to-efi-sig-list SIGNER.crt SIGNER.esl'
> > +                       % (data_dir, EFITOOLS_PATH), shell=True)
> > +
> > +            # Update dtb adding capsule certificate
> > +            check_call('cd %s; '
> > +                       'cp %s/test/py/tests/test_efi_capsule/signature.dts .'
> > +                       % (data_dir, u_boot_config.source_dir), shell=True)
> > +            check_call('cd %s; '
> > +                       'dtc -@ -I dts -O dtb -o signature.dtbo signature.dts; '
> > +                       'fdtoverlay -i %s/arch/sandbox/dts/test.dtb '
> > +                            '-o test_sig.dtb signature.dtbo'
> > +                       % (data_dir, u_boot_config.build_dir), shell=True)
> > +
> > +            # Create *malicious* private key (SIGNER2.key) and certificate
> > +            # (SIGNER2.crt)
> > +            check_call('cd %s; '
> > +                       'openssl req -x509 -sha256 -newkey rsa:2048 '
> > +                            '-subj /CN=TEST_SIGNER/ -keyout SIGNER2.key '
> > +                            '-out SIGNER2.crt -nodes -days 365'
> > +                       % data_dir, shell=True)
> > +
> >           # Create capsule files
> >           # two regions: one for u-boot.bin and the other for u-boot.env
> >           check_call('cd %s; echo -n u-boot:Old > u-boot.bin.old; echo -n u-boot:New > u-boot.bin.new; echo -n u-boot-env:Old -> u-boot.env.old; echo -n u-boot-env:New > u-boot.env.new' % data_dir,
> > @@ -56,6 +86,22 @@ def efi_capsule_data(request, u_boot_config):
> >           check_call('cd %s; %s/tools/mkeficapsule --raw u-boot.bin.new --index 1 Test02' %
> >                      (data_dir, u_boot_config.build_dir),
> >                      shell=True)
> > +        if capsule_auth_enabled:
> > +            # firmware signed with proper key
> > +            check_call('cd %s; '
> > +                       '%s/tools/mkeficapsule --index 1 --monotonic-count 1 '
> > +                            '--private-key SIGNER.key --certificate SIGNER.crt '
> > +                            '--raw u-boot.bin.new Test11'
> > +                       % (data_dir, u_boot_config.build_dir),
> > +                       shell=True)
> > +            # firmware signed with *mal* key
> > +            check_call('cd %s; '
> > +                       '%s/tools/mkeficapsule --index 1 --monotonic-count 1 '
> > +                            '--private-key SIGNER2.key '
> > +                            '--certificate SIGNER2.crt '
> > +                            '--raw u-boot.bin.new Test12'
> > +                       % (data_dir, u_boot_config.build_dir),
> > +                       shell=True)
> > 
> >           # Create a disk image with EFI system partition
> >           check_call('virt-make-fs --partition=gpt --size=+1M --type=vfat %s %s' %
> > diff --git a/test/py/tests/test_efi_capsule/signature.dts b/test/py/tests/test_efi_capsule/signature.dts
> > new file mode 100644
> > index 000000000000..078cfc76c93c
> > --- /dev/null
> > +++ b/test/py/tests/test_efi_capsule/signature.dts
> > @@ -0,0 +1,10 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +
> > +/dts-v1/;
> > +/plugin/;
> > +
> > +&{/} {
> > +	signature {
> > +		capsule-key = /incbin/("SIGNER.esl");
> > +	};
> > +};
> > diff --git a/test/py/tests/test_efi_capsule/test_capsule_firmware_signed.py b/test/py/tests/test_efi_capsule/test_capsule_firmware_signed.py
> > new file mode 100644
> > index 000000000000..593b032e9015
> > --- /dev/null
> > +++ b/test/py/tests/test_efi_capsule/test_capsule_firmware_signed.py
> > @@ -0,0 +1,254 @@
> > +# SPDX-License-Identifier:      GPL-2.0+
> > +# Copyright (c) 2021, Linaro Limited
> > +# Author: AKASHI Takahiro <takahiro.akashi@linaro.org>
> > +#
> > +# U-Boot UEFI: Firmware Update (Signed capsule) Test
> > +
> > +"""
> > +This test verifies capsule-on-disk firmware update
> > +with signed capsule files
> > +"""
> > +
> > +import pytest
> > +from capsule_defs import CAPSULE_DATA_DIR, CAPSULE_INSTALL_DIR
> > +
> > +@pytest.mark.boardspec('sandbox')
> > +@pytest.mark.buildconfigspec('efi_capsule_firmware_raw')
> > +@pytest.mark.buildconfigspec('efi_capsule_authenticate')
> > +@pytest.mark.buildconfigspec('dfu')
> > +@pytest.mark.buildconfigspec('dfu_sf')
> > +@pytest.mark.buildconfigspec('cmd_efidebug')
> > +@pytest.mark.buildconfigspec('cmd_fat')
> > +@pytest.mark.buildconfigspec('cmd_memory')
> > +@pytest.mark.buildconfigspec('cmd_nvedit_efi')
> > +@pytest.mark.buildconfigspec('cmd_sf')
> > +@pytest.mark.slow
> > +class TestEfiCapsuleFirmwareSigned(object):
> > +    def test_efi_capsule_auth1(
> > +            self, u_boot_config, u_boot_console, efi_capsule_data):
> > +        """
> > +        Test Case 1 - Update U-Boot on SPI Flash, raw image format
> > +                      0x100000-0x150000: U-Boot binary (but dummy)
> > +
> > +                      If the capsule is properly signed, the authentication
> > +                      should pass and the firmware be updated.
> > +        """
> > +        disk_img = efi_capsule_data
> > +        with u_boot_console.log.section('Test Case 1-a, before reboot'):
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'efidebug boot add -b 1 TEST host 0:1 /helloworld.efi',
> > +                'efidebug boot order 1',
> > +                'env set -e -nv -bs -rt OsIndications =0x0000000000000004',
> > +                'env set dfu_alt_info '
> > +                        '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                        '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                'env save'])
> > +
> > +            # initialize content
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'fatload host 0:1 4000000 %s/u-boot.bin.old'
> > +                        % CAPSULE_DATA_DIR,
> > +                'sf write 4000000 100000 10',
> > +                'sf read 5000000 100000 10',
> > +                'md.b 5000000 10'])
> > +            assert 'Old' in ''.join(output)
> > +
> > +            # place a capsule file
> > +            output = u_boot_console.run_command_list([
> > +                'fatload host 0:1 4000000 %s/Test11' % CAPSULE_DATA_DIR,
> > +                'fatwrite host 0:1 4000000 %s/Test11 $filesize'
> > +                        % CAPSULE_INSTALL_DIR,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test11' in ''.join(output)
> > +
> > +        # reboot
> > +        mnt_point = u_boot_config.persistent_data_dir + '/test_efi_capsule'
> > +        u_boot_console.config.dtb = mnt_point + CAPSULE_DATA_DIR \
> > +                                    + '/test_sig.dtb'
> > +        u_boot_console.restart_uboot()
> > +
> > +        capsule_early = u_boot_config.buildconfig.get(
> > +            'config_efi_capsule_on_disk_early')
> > +        with u_boot_console.log.section('Test Case 1-b, after reboot'):
> > +            if not capsule_early:
> > +                # make sure that dfu_alt_info exists even persistent variables
> > +                # are not available.
> > +                output = u_boot_console.run_command_list([
> > +                    'env set dfu_alt_info '
> > +                            '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                            '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                    'host bind 0 %s' % disk_img,
> > +                    'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +                assert 'Test11' in ''.join(output)
> > +
> > +                # need to run uefi command to initiate capsule handling
> > +                output = u_boot_console.run_command(
> > +                    'env print -e Capsule0000')
> > +
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test11' not in ''.join(output)
> > +
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'sf read 4000000 100000 10',
> > +                'md.b 4000000 10'])
> > +            assert 'u-boot:New' in ''.join(output)
> > +
> > +    def test_efi_capsule_auth2(
> > +            self, u_boot_config, u_boot_console, efi_capsule_data):
> > +        """
> > +        Test Case 2 - Update U-Boot on SPI Flash, raw image format
> > +                      0x100000-0x150000: U-Boot binary (but dummy)
> > +
> > +                      If the capsule is signed but with an invalid key,
> > +                      the authentication should fail and the firmware
> > +                      not be updated.
> > +        """
> > +        disk_img = efi_capsule_data
> > +        with u_boot_console.log.section('Test Case 2-a, before reboot'):
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'efidebug boot add -b 1 TEST host 0:1 /helloworld.efi',
> > +                'efidebug boot order 1',
> > +                'env set -e -nv -bs -rt OsIndications =0x0000000000000004',
> > +                'env set dfu_alt_info '
> > +                        '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                        '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                'env save'])
> > +
> > +            # initialize content
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'fatload host 0:1 4000000 %s/u-boot.bin.old'
> > +                        % CAPSULE_DATA_DIR,
> > +                'sf write 4000000 100000 10',
> > +                'sf read 5000000 100000 10',
> > +                'md.b 5000000 10'])
> > +            assert 'Old' in ''.join(output)
> > +
> > +            # place a capsule file
> > +            output = u_boot_console.run_command_list([
> > +                'fatload host 0:1 4000000 %s/Test12' % CAPSULE_DATA_DIR,
> > +                'fatwrite host 0:1 4000000 %s/Test12 $filesize'
> > +                                % CAPSULE_INSTALL_DIR,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test12' in ''.join(output)
> > +
> > +        # reboot
> > +        mnt_point = u_boot_config.persistent_data_dir + '/test_efi_capsule'
> > +        u_boot_console.config.dtb = mnt_point + CAPSULE_DATA_DIR \
> > +                                    + '/test_sig.dtb'
> > +        u_boot_console.restart_uboot()
> > +
> > +        capsule_early = u_boot_config.buildconfig.get(
> > +            'config_efi_capsule_on_disk_early')
> > +        with u_boot_console.log.section('Test Case 2-b, after reboot'):
> > +            if not capsule_early:
> > +                # make sure that dfu_alt_info exists even persistent variables
> > +                # are not available.
> > +                output = u_boot_console.run_command_list([
> > +                    'env set dfu_alt_info '
> > +                        '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                        '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                    'host bind 0 %s' % disk_img,
> > +                    'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +                assert 'Test12' in ''.join(output)
> > +
> > +                # need to run uefi command to initiate capsule handling
> > +                output = u_boot_console.run_command(
> > +                    'env print -e Capsule0000')
> > +
> > +            # deleted any way
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test12' not in ''.join(output)
> > +
> > +            # TODO: check CapsuleStatus in CapsuleXXXX
> > +
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'sf read 4000000 100000 10',
> > +                'md.b 4000000 10'])
> > +            assert 'u-boot:Old' in ''.join(output)
> > +
> > +    def test_efi_capsule_auth3(
> > +            self, u_boot_config, u_boot_console, efi_capsule_data):
> > +        """
> > +        Test Case 3 - Update U-Boot on SPI Flash, raw image format
> > +                      0x100000-0x150000: U-Boot binary (but dummy)
> > +
> > +                      If the capsule is not signed, the authentication
> > +                      should fail and the firmware not be updated.
> > +        """
> > +        disk_img = efi_capsule_data
> > +        with u_boot_console.log.section('Test Case 3-a, before reboot'):
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'efidebug boot add -b 1 TEST host 0:1 /helloworld.efi',
> > +                'efidebug boot order 1',
> > +                'env set -e -nv -bs -rt OsIndications =0x0000000000000004',
> > +                'env set dfu_alt_info '
> > +                        '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                        '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                'env save'])
> > +
> > +            # initialize content
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'fatload host 0:1 4000000 %s/u-boot.bin.old'
> > +                        % CAPSULE_DATA_DIR,
> > +                'sf write 4000000 100000 10',
> > +                'sf read 5000000 100000 10',
> > +                'md.b 5000000 10'])
> > +            assert 'Old' in ''.join(output)
> > +
> > +            # place a capsule file
> > +            output = u_boot_console.run_command_list([
> > +                'fatload host 0:1 4000000 %s/Test02' % CAPSULE_DATA_DIR,
> > +                'fatwrite host 0:1 4000000 %s/Test02 $filesize'
> > +                            % CAPSULE_INSTALL_DIR,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test02' in ''.join(output)
> > +
> > +        # reboot
> > +        mnt_point = u_boot_config.persistent_data_dir + '/test_efi_capsule'
> > +        u_boot_console.config.dtb = mnt_point + CAPSULE_DATA_DIR \
> > +                                    + '/test_sig.dtb'
> > +        u_boot_console.restart_uboot()
> > +
> > +        capsule_early = u_boot_config.buildconfig.get(
> > +            'config_efi_capsule_on_disk_early')
> > +        with u_boot_console.log.section('Test Case 3-b, after reboot'):
> > +            if not capsule_early:
> > +                # make sure that dfu_alt_info exists even persistent variables
> > +                # are not available.
> > +                output = u_boot_console.run_command_list([
> > +                    'env set dfu_alt_info '
> > +                            '"sf 0:0=u-boot-bin raw 0x100000 '
> > +                            '0x50000;u-boot-env raw 0x150000 0x200000"',
> > +                    'host bind 0 %s' % disk_img,
> > +                    'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +                assert 'Test02' in ''.join(output)
> > +
> > +                # need to run uefi command to initiate capsule handling
> > +                output = u_boot_console.run_command(
> > +                    'env print -e Capsule0000')
> > +
> > +            # deleted any way
> > +            output = u_boot_console.run_command_list([
> > +                'host bind 0 %s' % disk_img,
> > +                'fatls host 0:1 %s' % CAPSULE_INSTALL_DIR])
> > +            assert 'Test02' not in ''.join(output)
> > +
> > +            # TODO: check CapsuleStatus in CapsuleXXXX
> > +
> > +            output = u_boot_console.run_command_list([
> > +                'sf probe 0:0',
> > +                'sf read 4000000 100000 10',
> > +                'md.b 4000000 10'])
> > +            assert 'u-boot:Old' in ''.join(output)
> 

  reply	other threads:[~2022-02-14  0:43 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-02-09 10:10 [PATCH v11 0/9] efi_loader: capsule: improve capsule authentication support AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 1/9] tools: build mkeficapsule with tools-only_defconfig AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 2/9] tools: mkeficapsule: add firmware image signing AKASHI Takahiro
2022-02-11 19:16   ` Heinrich Schuchardt
2022-02-14  0:54     ` AKASHI Takahiro
2022-02-19 23:11       ` Simon Glass
2022-02-21  0:43         ` AKASHI Takahiro
2022-02-21 18:59           ` Heinrich Schuchardt
2022-03-13  6:05             ` Simon Glass
2022-02-09 10:10 ` [PATCH v11 3/9] tools: mkeficapsule: add man page AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 4/9] doc: update UEFI document for usage of mkeficapsule AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 5/9] test/py: efi_capsule: add image authentication test AKASHI Takahiro
2022-02-11 19:25   ` Heinrich Schuchardt
2022-02-14  0:43     ` AKASHI Takahiro [this message]
2022-02-16  8:40       ` Heinrich Schuchardt
2022-02-09 10:10 ` [PATCH v11 6/9] tools: mkeficapsule: allow for specifying GUID explicitly AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 7/9] test/py: efi_capsule: align with the syntax change of mkeficapsule AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 8/9] test/py: efi_capsule: add a test for "--guid" option AKASHI Takahiro
2022-02-09 10:10 ` [PATCH v11 9/9] test/py: efi_capsule: check the results in case of CAPSULE_AUTHENTICATE AKASHI Takahiro

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=20220214004306.GA39639@laputa \
    --to=takahiro.akashi@linaro.org \
    --cc=ilias.apalodimas@linaro.org \
    --cc=mark.kettenis@xs4all.nl \
    --cc=masami.hiramatsu@linaro.org \
    --cc=sjg@chromium.org \
    --cc=sughosh.ganu@linaro.org \
    --cc=u-boot@lists.denx.de \
    --cc=xypron.glpk@gmx.de \
    /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.