All of lore.kernel.org
 help / color / mirror / Atom feed
From: Stephen Warren <swarren-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
To: Jimmy Zhang <jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
Cc: amartin-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org,
	swarren-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org,
	linux-tegra-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
Subject: Re: [cbootimage PATCH v1 4/8] Add new configuration keyword "PkcKey"
Date: Mon, 21 Sep 2015 15:41:20 -0600	[thread overview]
Message-ID: <56007980.6060203@wwwdotorg.org> (raw)
In-Reply-To: <1441228760-26042-5-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>

On 09/02/2015 03:19 PM, Jimmy Zhang wrote:
> Use "PkcKey" to specify rsa key filename and load in
> rsa private keys from file.
>
> When keyword "PkcKey" is present, rsa pss signatures are generated
> for both bootloader and bct. rsa pubkey will also be filled into bct.

Oh, so the config file can either specify a literal value for all the 
hashes, or specify a key and have cbootimage generate them? It would be 
good to add some form of high-level documentation of the use-cases into 
README.txt

> PkcKey syntax:
>
>    PkcKey = <rsa_key_filename> [, --save];
>
> Examples:
>
>     PkcKey = rsa_priv.txt;
>       Load in keys from file rsa_priv.txt
>
>     PkcKey = rsa_priv.pem, --save;
>       Load in keys from file rsa_priv.pem and save pubkey and pubkey hash
>       value to file pubkey.mod and pubkey.sha respectively.

Can't the output filenames be either specified in the config file, or 
derived from the filename passed to PkcKey (so rsa_priv.pem -> 
rsa_priv.mod, rsa_priv.sha, rather than presumably hard-coding 
"pubkey.mod", "pubkey.sha")?

> Two key formats are supported.
>     1. Polar SSL format
>        P = ...
>        Q = ...
>
>     2. Open SSL format
>         -----BEGIN RSA PRIVATE KEY-----
>         ...
>         -----END RSA PRIVATE KEY-----

Do we need to support two formats; can't the openssl (or other) cmdline 
application convert the data file between the formats to reduce the 
complexity in cbootimage?

> diff --git a/src/cbootimage.h b/src/cbootimage.h

> +typedef enum
> +{
> +	false = 0,
> +	true = 1,
> +} bool;

This is a standard type; the definition should come from a standard 
system header file.


> @@ -110,6 +117,7 @@ typedef struct build_image_context_rec
>
>   	char *bct_filename;
>   	char *rsa_filename;
> +	void *pkckey;

Please expand on what this variable means (comment or better field name).

> diff --git a/src/crypto.c b/src/crypto.c

> - * Copyright (c) 2012, NVIDIA CORPORATION.  All rights reserved.
> + * Copyright (c) 2015, NVIDIA CORPORATION.  All rights reserved.

Please don't delete old copyright dates, but just add to them. i.e. 
"2012, 2015".

> +#include "libm/pkcs1-rsa.h"
> +#include "libm/hash.h"

I would expect libm/include/ to be in the #include path via a -I 
directive, so those would be:

#include <mcrypto/pkcs1-rsa.h>
#include <mcrypto/hash.h>

(At least the examples that ship with libmcrypto appear to expect the 
library gets installed with the <libmcrypto/> path available, so we 
should match that in any code using libmcrypto)

> +static u_int8_t *pkc_get_pubkey(void *key)
> +{
> +	NvTegraPkcKey *pPkcKey = (NvTegraPkcKey *)key;
> +	return (u_int8_t *)pPkcKey->Modulus.Number;
> +}

pkc_get_pubkey_modulus()? Since presumably *pPkcKey contains more than 
just the Modulus field?

> +int
> +pkc_sign_buffer(void *key, u_int8_t *buffer, u_int32_t size, u_int8_t **pSign)
...
> +	/* TODO: define constant for ssk.len */
> +	ssk.len = (UINT)pPkcKey->Modulus.Digits; /* length in digits of modulus in term of 32 bits */

What is the implication of this TODO? Why isn't this dynamic assignment 
acceptable?

> @@ -283,17 +321,116 @@ sign_bct(build_image_context *context,

> -	e = sign_data_block(bct + Offset, length, hash_buffer);
> -	if (e != 0)
> +	if ((e = sign_data_block(bct + Offset, length, hash_buffer)) != 0)
>   		goto fail;

The original code was cleaner.

> -	e = g_soc_config->set_data(token_crypto_hash,
> +	if ((e = g_soc_config->set_data(token_crypto_hash,

Same here. It's much better to assign the return code to a variable then 
test that variable rather than smash everything together into one 
hard-to-read expression. The same comment applies elsewhere too.

> +	/*  pkc signing? */
> +	if (context->pkckey) {
...
> +		g_soc_config->set_value(token_pub_key_modulus,
> +					pkc_get_pubkey(context->pkckey),
> +					bct);
> +	}
> +
> + fail:
> +	free(hash_buffer);
> +	return e;
> +}
> +
>   		goto fail;
>
>    fail:
>   	free(hash_buffer);
>   	return e;
>   }
> +void
> +SwapEndianness(

Something is wrong with the indentation at the end of that function, and 
there should be a blank line between the functinos.

> +int
> +pkc_savefile(const char *pFilename,
> +		u_int8_t *buffer, size_t bytes, const char* ext)
> +{
> +	int ret = 0;
> +	char *fn = malloc(sizeof(pFilename) + sizeof(ext) + 1);

sizeof yields the size of the pointer. I suspect you mean strlen.

> +	FILE *output_file;
> +
> +	sprintf(fn, "%s%s", pFilename, ext);
> +
> +	printf("Saving file %s\n", fn);
> +	output_file = fopen(fn, "w+");

I assume this is a binary file given the use of fwrite() below. I don't 
see any mixture of reading and writing. So that should be "wb".

> +	if (output_file == NULL) {
> +		printf("Error opening raw file %s.\n", fn);
> +		ret = -1;
> +		goto fail;
> +        }

Indentation is wrong there; I suggest scanning all the patches for 
TABs-vs-spaces.


> +int
> +pkc_save_pubkey(
> +	void *pKey,
> +	const char *pFileName)

I think this is saving the modulus and hash, not the key itself? So, 
pkc_save_pubkey_modulus_and_hash()?


> diff --git a/src/crypto.h b/src/crypto.h

> +#define PUBKEY_FILENAME		"pubkey."

that shouldn't be hard-coded.

> +#define PUBKEY_MODULUS		"mod"
> +#define PUBKEY_MODULUS_HASH	"sha"

Those names don't imply they're filename extensions. 
PUBKEY_FILENAME_EXT_{MODULUS,HASH} would be better.

> +#define BCT_FILENAME	"bct."
> +#define BL_FILENAME	"bl."

FILENAME_PREFIX/SUFFIX?

> +#define EXT_SIGNATURE	"sig"

FILENAME_EXT_SIGNATURE?

> +#define NV_BIGINT_MAX_DW 64

A comment re: why that's the right max size would be useful.

> +}NvTegraPkcsVersion;
> +}NvTegraPkcKey;

Missing space after }.

> diff --git a/src/data_layout.c b/src/data_layout.c

> @@ -620,6 +620,24 @@ write_image(build_image_context *context, file_type image_type)
>   					token_bl_crypto_hash,
>   					(u_int32_t*)hash_buffer,

> +				/* save bl sig to file bl.sig */
> +				pkc_savefile(BL_FILENAME, (u_int8_t *)sign,
> +						RSA_KEY_BYTE_SIZE, EXT_SIGNATURE);

This filename also shouldn't be hard-coded.

> diff --git a/src/parse.c b/src/parse.c

> +static int
> +parse_pkckey_file(build_image_context *context, parse_token token, char *rest)

> +	/* check save option */
> +	if (*rest == ',') {
> +		++rest;
> +
> +		/* Parse option. */
> +		if (strstr(rest, OPTION_SAVE) != NULL)
> +			save_key = 1;

This should error out if there's any other unexpected garbage.

> +	}

else error?

> diff --git a/src/rsa_key_parse.c b/src/rsa_key_parse.c

> +static void pkc_string_to_prime(u_int8_t *pBuffer, u_int8_t *pPrimeNum)
> +{
> +	u_int32_t i = 0;
> +
> +	while (i < (RSA_KEY_BYTE_SIZE * 2)) {
> +		*pPrimeNum = HEX_TO_DEC(pBuffer[i]) * 16 +
> +			HEX_TO_DEC(pBuffer[i + 1]);
> +		i += 2;
> +		pPrimeNum++;
> +	}

What if the string is too short or has an odd length; are those problems 
possible?


> +static bool
> +pkc_extract_polar_ssl_primes(

> +	/* Parse POLARSSL format */
> +	pTokenP = (u_int8_t *)strstr((char *)pBuffer, PRIME_PATTERN_P);
> +	pTokenQ = (u_int8_t *)strstr((char *)pBuffer, PRIME_PATTERN_Q);
> +
> +	if (pTokenP != NULL && pTokenQ != NULL) {

There would be fewer indentation levels if this was:

if (!pTokenP || !pTokenQ)
     return false;

> +		printf("PKC key in PolarSSL format\n");

Shouldn't that be debug output only? The tool shouldn't be noisy all the 
time.

> +static int
> +NvTegraPkcAsnParser(
> +    NvU8 *pBuf,
> +    NvU32 BufSize,
> +    NvU8 *pP,
> +    NvU8 *pQ)
> +{
> +    NvS32 i = 0;
> +    NvS32 LocalSize = 0;
> +    NvU32 Index = 0;
> +    NvU32 SequenceNum = 0;
> +    NvU32 IntegerNum = 0;
> +    NvU32 Expo = 0;
> +    NvU8 Type = 0;

All these initializations just hide used-before-initialized bugs. Can 
you please try removing them? The same comment applies elsewhere.

> +            case INTEGER:                       /*  INTEGER */
> +                switch(IntegerNum)
> +                {
> +                    case 0:     /*  PrivateKeyInfo version  */
> +                    case 1:     /*  PrivateKey version  */
> +                    case 3:     /*  Modulus */
...
> +                    case 2:     /*  Public Exponent */

All those instances of multiple spaces should be collapsed to just 1. 
The same comment may apply elsewhere.

> +                    case 4:     /*  P */
...
> +                        for (i = 0, Index++; i < LocalSize; i++)
> +                        {
> +                            *(pP++) = pBuf[Index++];
> +                        }

That looks like memcpy. {} aren't needed.

> +                    case 5:     /*  Q */

Cases 4 and 5 are identical, save for whether writing to pP or pQ. Can 
they be combined?

> +            default:
> +                /*  Ideally control should not come here for a .pk8 file */
> +                return NvError_BadParameter;

"Ideally" implies "should not happen, but we accept it if it happens". 
This seems to error out if that happens, which is a strong assertion.

> +static bool
> +pkc_extract_open_ssl_primes(

> +    if ((memcmp(pBuffer, DerFormat, sizeof(DerFormat)) != 0) &&
> +        (memcmp(pBuffer, PEMFORMAT_START, sizeof(PEMFORMAT_START) - 1) != 0))
> +    {
> +        return false;
> +    }

Is it guaranteed that in PEM format, there are no blank lines or other 
data at the start/end of the file? I don't expect so since that's the 
entire point of the large strings that are used as PEM format markers, 
but perhaps.

> +    DerKeySize = BufSize - sizeof(PEMFORMAT_END);
> +    pPemStart = pBuffer + sizeof(PEMFORMAT_START);
> +
> +    if (memcmp(pBuffer + DerKeySize, PEMFORMAT_END,
> +        sizeof(PEMFORMAT_END) - 1) != 0)
> +    {
> +        return false;
> +    }

What if the file is shorter than DerKeySize + strlen(PEMFORMAT_START) + 
strlen(PEMFORMAT_END)?

> +    printf("PKC key in Open SSL format\n");
> +
> +    if (memcmp(pBuffer, DerFormat, sizeof(DerFormat)) != 0)
> +    {

Please be consistent about wrapping/not-wrapping braces. I think this 
project uses wrapped braces.

> +        DerKeySize = DerKeySize - sizeof(PEMFORMAT_START);
> +
> +        e = NvBase64Decode(pPemStart, DerKeySize, NULL, &Base64Size);
> +	if (e != NvSuccess) {
> +		printf("Base 64 decoding size failed\n");
> +		goto fail;
> +	}
> +
> +
> +        pBase64Buf = malloc(Base64Size);

Two blank lines there, and indentation issues throughout the function.

> +fail:
> +
> +    if (pBase64Buf != NULL)
> +        free(pBase64Buf);

free() handles NULL.

> +    if (e != NvSuccess)
> +        return false;
> +
> +    return true;

that's: return e == NvSuccess;

> +static bool
> +pkc_extract_private_keys(
> +	u_int8_t *pBuffer,
> +	u_int32_t BufSize,
> +	u_int8_t *pP,
> +	u_int8_t *pQ)
> +{
> +	return
> +	((pkc_extract_open_ssl_primes(pBuffer, BufSize, pP, pQ) == true) ||
> +	(pkc_extract_polar_ssl_primes(pBuffer, BufSize, pP, pQ) == true));

Wonky indentation. No need for "== true";

> +static NvU32 NvInverseDigit(NvU32 x)
> +{
> +    NvU32 i = 1;
> +    NvU32 j = 2;
> +    do
> +    {
> +        i ^= (j ^ (j & (x * i)));
> +        j += j;
> +    }
> +    while (j);

"while" should be wrapped with }

What does this function do and how/why? A comment is needed.

> +static NvU32
> +NvBigIntIsZero(
> +    const NvU32 *x,
> +    const NvU32 Digits)
> +{
> +    NvU32 i;
> +    for (i = 0; i < Digits; i++)
> +    {
> +        if (x[i] != 0)
> +        {
> +            return NV_FALSE;
> +        }

No need for {}.

> +static NvU32
> +NvBigIntSubtract(
> +    NvU32 *z,
> +    const NvU32 *x,
> +    const NvU32 *y,
> +    const NvU32 Digits)
> +{
> +    NvU32 i, j, k;
> +    for (i = k = 0; i < Digits; i ++)
> +    {
> +        j = x[i] - k;
> +        k = (j > (~k)) ? 1 : 0;
> +        j -= y[i];
> +        k += (j > (~y[i])) ? 1 : 0;
> +        z[i] = j;
> +    }
> +    return k;

">" should evaluate to 1 or 0, so you can drop the ?:.

It would help to use sane variable names, e.g k -> borrow, j -> tmp or 
something like that.

I would also have expected simpler checks for borrow and only a single 
check to be necessary. i.e. a final "if (j > x[i])". I assume that an 
NvBigInt is unsigned? NvBigIntcompare's implementation seems to imply so.

I wonder why all these math functions are re-implemented rather than 
making use of the "big digits" code in libmcrypto? Where did all this 
math code come from?

> +static NvU32 NvBigIntGetBit(const NvU32 *x, NvU32 i)
> +{
> +  NvU32 b = 0;
> +
> +  return (((x[i >> 5] >> (i & 0x1f)) ^ b) & 1);

Why "^ b" when b is hard-coded to 0?

I haven't reviewed the rest of the math functions in any way.

> +NvBase64Decode(

> +    if (pOutBuf == NULL)
> +    {
> +        /* Valid if the caller is requesting the size of the decoded
> +         * binary buffer so they know how much to alloc.
> +         */
> +        *pOutBufSize = 0;
> +    }
> +    else
> +    {
> +        /* Validate the size of the passed input data buffer.
> +         * In theory the input buffer size should be 3/4 the size of
> +         * the decoded buffer. But if there were some white
> +         * space chars in input buffer then it is possible that the
> +         * input buffer is smaller.
> +         * First validate against 2/3rds the size of the input buffer
> +         * here. That allows for some slop.
> +         * Below code makes sure output buffer size is big enough.
> +         */
> +        if (*pOutBufSize < (InBufSize * 2)/3)

If there's a known maximal minimum size for the buffer, why not just 
return that in the !pOutBuf case rather than executing the big/slow loop 
below?

> +    /* This loop is less efficient than it could be because
> +     * it's designed to tolerate bad characters (like whitespace)
> +     * in the input buffer.
> +     */
> +    while (i < InBufSize)
> +    {
> +        NvU8 CurrVal;
> +        NvU32 ValidLen = 0;
> +        NvU8 ValidBuf[4];
> +
> +        // gather 4 good chars from the pInBufput stream

Inconsistent comment style.

> +        if (pOutBuf == NULL)
> +        {
> +            // just measurpInBufg the size of the destpInBufation buffer

Search/replace typos there.

> +        switch (ValidLen)
> +        {
> +            case 4:
> +                // 4 pInBufput chars equals 3 pOutBufput chars
> +                pOutBuf[2] = (unsigned char ) (((ValidBuf[2] << 6) & 0xc0) | ValidBuf[3]);
> +                // fall through
> +            case 3:
> +                // 3 pInBufput chars equals 2 pOutBufput chars
> +                pOutBuf[1] = (unsigned char ) (ValidBuf[1] << 4 | ValidBuf[2] >> 2);
> +                // fall through
> +            case 2:
> +                // 2 pInBufput chars equals 1 pOutBufput char
> +                pOutBuf[0] = (unsigned char ) (ValidBuf[0] << 2 | ValidBuf[1] >> 4);
> +                pOutBuf += ValidLen - 1;
> +                break;
> +            case 1:
> +                // Unexpected
> +                break;
> +            case 0:
> +                // conceivable if white space follows the end of valid data
> +                break;
> +            default:
> +                // Unexpected
> +                break;
> +        }

Shouldn't at least cases 1, return an error?

There are lots of formatting errors in this patch that I didn't 
explicitly call out. Lots of cleanup is required.

I didn't review the math code. It'd be best if it was replaced with 
something pre-existing rather than re-inventing the wheel.

Someone familiar with our chip security needs to review the crypto usage 
in this code.

> diff --git a/src/rsa_key_parse.h b/src/rsa_key_parse.h

> +#define HEX_TO_DEC(c)                                   \
> +    (((c) >= '0' && (c) <= '9') ? (c) - '0' :           \
> +     ((c) >= 'a' && (c) <= 'f') ? (c) - 'a' + 10 :      \
> +     ((c) >= 'A' && (c) <= 'F') ? (c) - 'A' + 10 : -1)

I believe that's already in libmcrypto's hex2byte().

> +/* Explicitly sized signed and unsigned ints. */
> +typedef unsigned char      NvU8;  /**< 0 to 255 */
> +typedef unsigned short     NvU16; /**< 0 to 65535 */
> +typedef unsigned int       NvU32; /**< 0 to 4294967295 */
> +typedef unsigned long long NvU64; /**< 0 to 18446744073709551615 */
> +typedef signed char        NvS8;  /**< -128 to 127 */
> +typedef signed short       NvS16; /**< -32768 to 32767 */
> +typedef signed int         NvS32; /**< -2147483648 to 2147483647 */
> +typedef signed long long   NvS64; /**< 2^-63 to 2^63-1 */

Let's not introduce even more types. cbootimage already has types for these.

> +#define NV_TRUE	1
> +#define NV_FALSE 0

Let's just use defines from system header files for these.

> +#define NvSuccess			0
> +#define NvError_BadParameter		-10
> +#define NvError_InsufficientMemory	-11

Can't we use error codes from <errno.h>?

> +#endif /* #ifndef INCLUDED_RSA_KEY_PARSE_H_N */

Just "#endif"; no need for the comment.

  parent reply	other threads:[~2015-09-21 21:41 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-09-02 21:19 [cbootimage PATCH v1 0/8] Add rsa pss signature support Jimmy Zhang
     [not found] ` <1441228760-26042-1-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-02 21:19   ` [cbootimage PATCH v1 1/8] Enable --update | -u option support for t210 Jimmy Zhang
     [not found]     ` <1441228760-26042-2-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 19:55       ` Stephen Warren
     [not found]         ` <5600609F.60002-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-21 23:07           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 2/8] Add bct_dump " Jimmy Zhang
     [not found]     ` <1441228760-26042-3-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 20:05       ` Stephen Warren
     [not found]         ` <56006319.5030009-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-21 23:27           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 3/8] Add in libmcrypto Jimmy Zhang
     [not found]     ` <1441228760-26042-4-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 20:11       ` Stephen Warren
     [not found]         ` <56006457.4040601-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-22  0:05           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 4/8] Add new configuration keyword "PkcKey" Jimmy Zhang
     [not found]     ` <1441228760-26042-5-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 21:41       ` Stephen Warren [this message]
     [not found]         ` <56007980.6060203-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-22 18:07           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 5/8] Fix some issues found in libmcrypto Jimmy Zhang
     [not found]     ` <1441228760-26042-6-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 22:08       ` Stephen Warren
     [not found]         ` <56007FEB.7010408-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-22  1:11           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 6/8] Add new configuration keyword "ReSignBl" Jimmy Zhang
     [not found]     ` <1441228760-26042-7-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 22:10       ` Stephen Warren
     [not found]         ` <5600804F.402-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-22  0:45           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 7/8] Add new command line option "--sign | -n" to sign binary image Jimmy Zhang
     [not found]     ` <1441228760-26042-8-git-send-email-jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org>
2015-09-21 22:13       ` Stephen Warren
     [not found]         ` <560080F8.9090008-3lzwWm7+Weoh9ZMKESR00Q@public.gmane.org>
2015-09-22  0:36           ` Jimmy Zhang
2015-09-02 21:19   ` [cbootimage PATCH v1 8/8] Bump to version 1.6 Jimmy Zhang
2015-09-08 17:19   ` [cbootimage PATCH v1 0/8] Add rsa pss signature support Jimmy Zhang

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=56007980.6060203@wwwdotorg.org \
    --to=swarren-3lzwwm7+weoh9zmkesr00q@public.gmane.org \
    --cc=amartin-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org \
    --cc=jimmzhang-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org \
    --cc=linux-tegra-u79uwXL29TY76Z2rM5mHXA@public.gmane.org \
    --cc=swarren-DDmLM1+adcrQT0dZR+AlfA@public.gmane.org \
    /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.