代码之家  ›  专栏  ›  技术社区  ›  Hassan Syed

endianness和c api:特别是openssl

  •  3
  • Hassan Syed  · 技术社区  · 14 年前

    我有一个使用以下openssl调用的算法:

    HMAC_update() / HMAC_final() // ripe160
    EVP_CipherUpdate() / EVP_CipherFinal() // cbc_blowfish
    

    这些算法 unsigned char * 进入“纯文本”。我的输入数据来自C++ std::string::c_str() 源于 protocol buffer 对象作为编码的UTF-8字符串。UTF-8字符串是用来作为尾数中性的。然而,我有点偏执于OpenSSL如何对数据执行操作。

    我的理解是加密算法可以处理8位数据块,如果 无符号字符* 用于指针算法,当执行操作时,算法应该是尾数中性的,我不需要担心任何问题。我的不确定性是由我在一个小的endian机器上工作,从来没有做过任何真正的跨体系结构编程这一事实造成的。

    我的信念/推理基于以下两个性质

    1. STD::字符串(不含二进制)内部使用一个8位PTR和一个结果 c_str() 无论CPU架构如何,ptr都会以同样的方式进行分配。
    2. 加密算法要么通过设计,要么通过实现,与endian无关。

    我知道获得明确答案的最好方法是使用 QEMU 做一些跨平台的单元测试(我计划这样做)。 我的问题是请求对我的推理进行评论,当遇到类似的问题时,可能会帮助其他程序员。

    4 回复  |  直到 14 年前
        1
  •  4
  •   Thomas Pornin    14 年前

    一些 密码算法,特别是哈希函数(在HMAC中使用),被指定在任意比特序列上操作。然而,在实际的物理计算机和大多数协议中,数据是 八位字节 :位数是8的倍数,可以按8位分组处理。一组8位在名义上是一个“八位字节”,但术语“字节”更常见。八位字节的数值介于0和255之间(含0和255)。在一些编程语言(例如Java)中,数值是在(128和127之间)签名的,但这是相同的概念。

    注意,在C编程语言的上下文中(如ISO 9899:1999标准中的定义,即“C标准”),a 字节 被定义为基本可寻址存储器单元,由 unsigned char 类型。 sizeof 返回以字节为单位的大小(因此, sizeof(unsigned char) 必须等于1)。 malloc() 采用字节大小。在c中,字节中的位数由 CHAR_BIT 宏(定义于 <limits.h> )且大于或等于8。在大多数计算机上,C字节中只有8位(即C字节是一个八位字节,每个人都称之为“字节”)。那里 有些系统的字节更大(通常是嵌入式DSP),但如果你有这样的系统,你会知道。

    因此,每一个在任意比特序列上工作的加密算法实际上都定义了如何将比特内部解释为八位字节(bytes)。这个 AES SHA 即使在挑剔的数学家眼中,规范也要花很长的时间才能正确地做到这一点。对于每一个实际情况,您的数据都已经是一个字节序列,并且假定已经发生了将位分组为字节的情况;所以您只需将字节输入到算法实现中,一切都很好。

    因此,在实践中,密码算法 实施 期望一个序列 字节 作为输入,并生成 字节 作为输出。

    终结性 (隐含在字节级别)是关于多字节值(需要对多个字节进行编码的值)如何排列成字节序列(即哪个字节排在第一位)的约定。UTF-8是尾数中性的,因为它已经定义了这种布局:当一个字符要编码成几个字节时,UTF-8要求这些字节中的哪一个是第一个,哪一个是最后一个。这就是为什么utf-8是“endian中性的”:字符到字节的转换是一个固定的约定,这不取决于本地硬件最喜欢读取或写入字节的方式。endianness通常与整数值在内存中的写入方式有关。

    关于跨平台编程: 经验是无法替代的。因此,在多个平台上尝试是一种好方法。通过使代码64位干净,即在32位和64位平台上正确运行相同的代码,您已经学到了很多东西。任何最近使用Linux的PC都能满足这个要求。现在,big-endian系统非常罕见;您需要一个较旧的Mac(一个带有PowerPC处理器的Mac)或几种Unix工作站中的一个(请记住SPARC系统或HP/UX下的Itanium系统)。较新的设计倾向于采用小endian约定。

    关于c中的endianness: 如果你的程序必须要担心结尾,那么很可能你做的不对。endianness是关于将整数(16位、32位或更多)转换为字节并返回。如果您的代码担心endianness,那么这意味着您的代码以整数形式写入数据,并以字节形式读取数据,反之亦然。不管怎样,您都在做一些“类型别名”:内存的某些部分是通过不同类型的多个指针访问的。这是 坏的 。它不仅降低了代码的可移植性,而且在要求编译器优化代码时,它也容易崩溃。

    在适当的C程序中,只有当值要写入或从文件或网络套接字中读取时,才会为I/O处理endianness。I/O遵循定义要使用的endianness的协议(例如,在TCP/IP中,通常使用big endian约定)。正确的方法是编写一些包装函数:

    uint32_t decode32le(const void *src)
    {
        const unsigned char *buf = src;
        return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8)
            | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
    }
    
    uint32_t decode32be(const void *src)
    {
        const unsigned char *buf = src;
        return (uint32_t)buf[3] | ((uint32_t)buf[2] << 8)
            | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[0] << 24);
    }
    
    void encode32le(void *dst, uint32_t val)
    {
        unsigned char *buf = dst;
        buf[0] = val;
        buf[1] = val >> 8;
        buf[2] = val >> 16;
        buf[3] = val >> 24;
    }
    
    void encode32be(void *dst, uint32_t val)
    {
        unsigned char *buf = dst;
        buf[3] = val;
        buf[2] = val >> 8;
        buf[1] = val >> 16;
        buf[0] = val >> 24;
    }
    

    可能,使这些功能 static inline “并将它们放在头文件中,以便编译器可以在调用代码时随意内联它们。

    然后,每当您想从一个新获得的(或即将写入的)文件或套接字的内存缓冲区中写入或读取32位整数时,都可以使用这些函数。这将使您的代码结束语中性(因此是可移植的),更清晰,从而更容易阅读、开发、调试和维护。而且在 非常罕见 在这种编码和解码成为瓶颈的情况下(只有在使用CPU非常弱、网络连接非常快的平台(即完全不是PC)时,才可能发生这种情况),您仍然可以用特定于体系结构的宏(可能是WI)替换这些功能的实现。不修改其余代码的内联程序集。

        2
  •  7
  •   Steve Jessop    14 年前

    UTF-8字符串和STD::字符串都被定义为字符序列。加密算法被定义为对字节/八位字节序列进行操作(在C字节中,A字符是相同的,如果您的字节不是八位字节,那么您正在执行一个异常的实现,您可能需要稍微小心处理UTF-8)。在连续内存中表示一个字节序列的唯一合理方法是,第一个字节在最低地址,随后的一个字节在更高的地址(C数组)。加密算法不关心字节代表什么,所以没关系。

    只有当你处理像 int 不是一个字节序列。抽象地说,它只是一个“东西”,它保存着int-min到int-max的值。当您在内存中表示这样一个野兽时,当然它必须是一个字节数,但是没有一种方法可以做到这一点。

    在实践中,如果您(可能通过您调用的某个东西)将char*重新解释为int*或反之亦然,或者定义一个协议,在该协议中,int使用一系列chars表示,那么endian ness在c中很重要。如果您只处理字符数组,或者只处理整数数组,那么这是不相关的,因为 endianness是ints的一个属性 以及其他比char大的类型。

        3
  •  2
  •   ereOn    14 年前

    似乎真正的问题是:

    “我能确定我编码的utf-8字符串在不同的计算机上将以相同的方式在内部表示吗?”

    因为,正如您所说,OpenSSL例程并没有真正解决这个问题(它们也不必知道)。

    因为你只要求评论,我认为你应该没事。无论计算机体系结构如何,对于两个相同的数据块,OpenSSL例程的行为应该相同。

        4
  •  0
  •   Adam W    14 年前

    一种确保endianes的方法是遵循IP标准 network byte order .

    看一看 here 对于您需要的功能。这些应该在Windows和*NIX上可用现代C++实现。

    不过,我相信你的推理是正确的,在这种情况下你不必担心。

    编辑:为了清楚起见,网络字节顺序注释假定您正在发送数据,并且担心在另一端如何接收数据。如果发送和接收都在同一台机器上,那么应该没有问题。