美文网首页
IOTA 基石 - ISS 签名算法详解

IOTA 基石 - ISS 签名算法详解

作者: 萝卜头4lbt | 来源:发表于2019-06-13 11:44 被阅读0次

    一、概要

        IOTA 消息签名 方案使用的是Winternitz一次签名算法(WOTS),它是一种hash 签名算法,该类算法在几十年前就被提出来了,不过一直没有什么人用,因为它基于该类算法生成的签名实在太长了,并且也没有什么明显的特点,不过因为该类算法被证明可以抵抗量子计算,所以,IOTA 则使用该类算法作为消息 签名的解决方案,不过该类算法存在一个明显的缺陷是每次签名都会暴露一般的私钥,因此,IOTA的账户使用体系会有别于别的经典的区块链账户体系,本章节主要就是详细分析IOTA 的ISS 实现 。

    二、详细介绍&解析

        在深入WOTS算法实现的前,我们先先、从最简单的一次签名算法Lamport一次性签名(LOTS)作为入门了解,然后在什么WOTS 的详细实现,最后在看看基于IOTA 是如何使用该算法实现消息验签的;因此,本章节按照以下三小节详细分析。

    1)LOTS原理
    2)WOTS详细实现
    3)WOTS使用

    2.1 Lamport原理

        首先我们通过随机算法随机生成一对私钥,每个私钥都包含256个随机数,这里每个随机数都取256bit大小,确实私钥是非常长的,如下

    private key

    然后我们将这一对私钥中每个随机数都进行hash,得到了公钥对

    public key

    接着,我们就可以开始签名了,对于文件M,首先计算得到它的hash摘要值,H(M),这里H(M)也是256bit长的,然后我们检查H(M)的每一个bit,对于第n个bit,当其为0时,我们就取私钥串1的第n个数,当其值为1时,我们就取私钥串2的第n个数,比如当文件M的hash为110...10时,情况就如下

    Signatures

    将红色块的数字合并就得到了文件M的签名。

        至于该签名的验证也非常简单,我们先计算出文件M的hash,H(M)。后依据同样的算法再从公钥对中取值,将公钥中的hash合并后看是否跟签名相同即可

        观察上面的签名过程,我们不难发现发布签名后实际上我们将私钥的一半公布出去了,哪怕是这样其实这种算法也是很安全的,因为攻击者并不知道另一半的私钥,除非他能破解该hash函数,从公钥推出私钥。

        不过如果你再次使用该私钥进行签名的话,那么又会随机暴露一半的私钥,相当于在之前没暴露的一半里再随机显示一半,这样你暴露的私钥就达到了75%,这样就非常危险了,攻击者已经有能力根据这暴露的私钥信息伪造签名了,所以说hash签名的地址一般都是一次性的,重复使用是不可取的,当然,也不是说就不存在地址可重复使用的hash签名技术,比如基于Merkle树的Merkle OTS方案,该方案使用的公钥是一串公钥对的根hash,事实上每次签名使用的依然是不同的私钥,而且该方案的签名长度更长,公钥对较多的话签名长度可能是Lamport方案的几倍,有兴趣的可以看看这篇文章,Hash based signatures


    2.2 WOTS详细实现

    2.2.1种子生成

        我们知道IOTA网络是无许可的网络类型,任何人都可以使用它并与之交互。在任何阶段都不需要中央集权管理。根据上述介绍,种子是给定地址的唯一密钥,任何拥有种子的人也拥有与各自IOTA地址相关的所有资金,而任何人都可以随时生成自己的种子和相应的私钥/地址。

    而种子和地址仅由字符[A-Z]和数字9组成,长度是固定的81个字符。在IOTA地址中通常还会使用另外9个字符(因此其总长度是90),这是一个校验和。它提供了一种防止在处理IOTA地址时出现错误操作的方法,我们先来深入种子的生成方式:

    public class SeedRandomGenerator {
    
        public static final String TRYTE_ALPHABET = "9ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
        /**
         * Generate a new seed.
         * @return Random generated seed.
         **/
        public static String generateNewSeed() {
             //将字母表转换成 char 数组
            char[] chars = TRYTE_ALPHABET.toCharArray();
            StringBuilder builder = new StringBuilder();
            // 使用SecureRandom 随机生成器
            SecureRandom random = new SecureRandom();
    
            // 随机生成字母表中,81个字符作为seed
            for (int i = 0; i < 81; i++) { 
                char c = chars[random.nextInt(chars.length)];
                builder.append(c);  // 按序组装
            } 
            return builder.toString(); // 返回seed 字符串
        }
    }
    

    上述代码比较简单,使用伪随机生成器,随机生成81 指定字符集 中的字符,一共3^243 种组合,虽然是随机生成的,但也可以保证唯一性了。

    2.2.2 私钥生成

        先分析私钥生成实现key(int[] inSeed, int index, int security):

    
        public int[] key(int[] inSeed, int index, int security) throws ArgumentException {
    
            // 确保security 的范围为[1,3]
            if (!InputValidator.isValidSecurityLevel(security)) {
                throw new ArgumentException(INVALID_SECURITY_LEVEL_INPUT_ERROR);
            }
            
            ...
            //step1,依据index 计算子seed
            int[] seed = subseed(inSeed, index);
            
            ICurl curl = this.getICurlObject(SpongeFactory.Mode.KERL);
            curl.reset();
            curl.absorb(seed, 0, seed.length);
            
            //step2,将hash 后的seed 结果覆盖到seed 中
            curl.squeeze(seed, 0, seed.length);
            curl.reset();
            // absorb subseed
            curl.absorb(seed, 0, seed.length);
            // 构建私钥,长度为security * 243 * 27
            final int[] key = new int[security * HASH_LENGTH * 27];
            
            final int[] buffer = new int[seed.length];
            int offset = 0;
            // step3、依据security反复对对新结果hash ,并填充至每一段
            while (security-- > 0) { 
                for (int i = 0; i < 27; i++) {
                    //将上次hash结果在hash,并将结果写入buffer
                    curl.squeeze(buffer, 0, seed.length);
                    //将hash 结果填充到指定段
                    System.arraycopy(buffer, 0, key, offset, HASH_LENGTH);
    
                    offset += HASH_LENGTH;
                }
            }
            return key;
        }
    
             /*
             * 
             * index 0 = [0,0,0,1,0,-1,0,1,1,0,-1,1,-1,0] // 原seed
             * index 1 = [1,0,0,1,0,-1,0,1,1,0,-1,1,-1,0] // 原seed + 1
             * index 2 = [-1,1,0,1,0,-1,0,1,1,0,-1,1,-1,0] // 原seed + 2
             * index 3 = [0,1,0,1,0,-1,0,1,1,0,-1,1,-1,0] // 原seed +3
             */
        public int[] subseed(int[] inSeed, int index) throws ArgumentException {
            // 防御式判断
            if (index < 0) {
                throw new ArgumentException(INVALID_INDEX_INPUT_ERROR);
            }
            
            int[] seed = inSeed.clone();
    
            // 执行 index 次 自增一
            for (int i = 0; i < index; i++) {
                for (int j = 0; j < seed.length; j++) {
                    if (++seed[j] > 1) {
                        seed[j] = -1;
                    } else {
                        break;
                    }
                }
            }
            return seed;
        }
    
    

        在分析上述获取私钥的源码实现前,我们先来了解以下入参,首先是inSeed,它是长度81的trytes种子 转成 长度为243 trits 的int 数组,种子的概念上面已详细分析; 而index 的作用就是对 inSeed 三进制数组进行作加法;security(默认情况下,iota 统一为2,可取值为1、2、3,级别越高越安全)就是对结果key进行多少次hash运算。而私钥的长度需要由security 来决定,具体值为security * 243 * 27。
        而依据index 计算subseed int[] seed = subseed(inSeed, index)核心就是对原inSeed 执行三进制数组自增index 次,具体的trytes、trits 以及各种转换、运算等,已在《IOTA基石 - 三进制系统之 Trit 和 Tryte》中详细分析,这里就不再深入。
        接着是如何通过seed 来分段求取私钥hash来求取私钥结果key。这里,为了方便描述,我们定义H(M) 为文本内M容的hash摘要,因此,由作出以下定义:

    H^1(M) = H(M)
    H^2(M) = H(H(M))
    H^3(M) = H(H^2(M))
    ...
    H^n(M) = H(H^n-1(M))

    这里的H 算法在本章节指的是Kerl,该算法在《IOTA 基石 - Sponge 算法详解》已详细分析,这里就不再深入。
        总结一下私钥key(...)的获取流程:

    • step1,根据入参inSeed 以及 index 获取seed;
    • step2,对step1 中的seed 结果进行顶一次hash,并覆盖原seed;
    • step3,依据security 以及 step 2 中的seed,求私钥结果key,具体的求取流程及结果见下图:
    private key
    2.2.3地址生成

        传统的以区块链为基础的系统,例如比特币,你的钱包地址是可以被多次重复使用的。但与之相反,IOTA的地址(在进行对外转账时)只能被使用一次。也就是说,一个IOTA地址如果只用来收账,可以使用无限次。但一旦当你使用这个地址向外转账完成后,就不应该再使用改地址了。这是因为,当你对外进行转账的时候(如果你发送的是IOTA),这个特定地址中的部分私有密钥被暴露,进而给了其他人(例如黑客)暴力破解全部密钥,进而最终获得存储在这个地址中的所有IOTA 的可能性。你通过同一个IOTA地址向外转账的次数越多,黑客就越容易暴力破解你的密钥。需要注意的是,获得一个地址的密钥不会暴露你的IOTA种子或是在你的种子(账户)中的其他地址的密钥。上述所描述的缺点是源于hash签名算法所导致的
        总之,对于一个IOTA地址,只要我们不对外进行转账操作(“向外发送”的操作),我们可以使用这个地址进行无限次的安全收账。但一旦你使用这个地址向外转账后,这个地址不应该再被使用了!
        下面,我们通过address 的生成源码来分析一下其实现:

    
        public static String newAddress(String seed, int security, int index, boolean checksum, ICurl curl) throws ArgumentException {
             ...
            Signing signing = new Signing(curl);
            // 先获取私钥
            final int[] key = signing.key(Converter.trits(seed), index, security);
            //对私钥二次 摘要
            final int[] digests = signing.digests(key);
            // 将再要转成addressTrits
            final int[] addressTrits = signing.address(digests);
            // 将addressTrits 转成 address
            String address = Converter.trytes(addressTrits);
            
            //拼接校验和
            if (checksum) {
                address = Checksum.addChecksum(address);
            }
            // 返回address
            return address;
        }
    

    我们来详细分析上述实现,首先是根据入参求取私钥key,前面一小节已详细分析。获取私钥后,通过signing.digests(key),求取二次摘要:

    
        public int[] digests(int[] key) throws ArgumentException {
            // 依据key长度 求 security (6561 = 27 * 243)
            int security = (int) Math.floor(key.length / 6561);
            ...
            //二次摘要结果存放
            int[] digests = new int[security * 243];
            //私钥段临时存放
            int[] keyFragment = new int[6561];
    
            ICurl curl = this.getICurlObject(SpongeFactory.Mode.KERL);
            // 分段处理
            for (int i = 0; i < = security; i++) {
                //
                System.arraycopy(key, i * 6561, keyFragment, 0, 6561);
    
                for (int j = 0; j < 27; j++) { // 求每一小段(243) 自身的hash
                    for (int k = 0; k < 26; k++) { // 对每一段反复hash 自身结果25次,并覆盖原小段
                        curl.reset()
                                .absorb(keyFragment, j * 243, 243)
                                .squeeze(keyFragment, j * 243, 243);
                    }
                }
                // 重设状态
                curl.reset();
                // 将keyFragment 的hash 结果输入至digests。
                curl.absorb(keyFragment, 0, keyFragment.length);
                curl.squeeze(digests, i * 243, 243);
            }
    
            return digests;
        }
    

    上述为私钥的二次摘要实现流程,二次摘要的大小同样依据security 决定,具体为243 * security,即每一段大小为243;根据上述分析,private key是分段,security 为段数, 每一段的大小为243 * 27,而每一段又由小段长度为 243 的int字节数组 组成。换言之,二次摘要 的段数 与 private key 的段数一致;具体的求取过程为,先将privete key 中的每一小段自身hash 26次,然后在将每一段(27 * 243) 作为整体再次输出长度为243 的hash 结果写入二次摘要结果digests 所对应的段数,具体的效果见下:


    digests

    二次摘要digests 求得后,通过int[] addressTrits = signing.address(digests)求 区块链地址的 三进制区块地址:

        public int[] address(int[] digests) {
            int[] address = new int[HASH_LENGTH];
            ICurl curl = this.getICurlObject(SpongeFactory.Mode.KERL);
            curl.reset()
                    .absorb(digests)
                    .squeeze(address);
            return address;
        }
    

    上述实现比较简单,无非将digests 作为输入,通过kerl hash 函数,输出长度为243 的三进制hash 值。到这里,三进制 区块地址addressTrits已得到,见下图:


    addressTrits

    接着,在通过Converter.trytes(addressTrits) 将三进制 转成我们稍微可读的,长度为81 的区块链地址。最后,通过截取address的hash 值的最后九位作为checksum,追加到远address 的尾部,返回给上层应用一个长度为90(81 + 9)的地址,用以下三幅图总结:

    summary-address

        依据图[summary-address]总结一下:

    • step1,先根据条件求private key,当然,其长度由security决定,即security 为段数,而一个完整的私钥段又由27 个 243 小段组成;
    • step2,依据private key 求digest,需要注意的是step1 中的 H^N(M) 等于 step2 中的 fragmentN(N 为1、2、3...);
    • step3,最后,在依据step2 中的digest 求 addressTrits。

        到这里,地址生成源码分析完毕。

    2.2.4ISS签名

        一般来说,签名流程都是先对需要签名的内容通过hash 函数求取其签名内容的摘要hash(content),然后,在使用指定的签名算法对内容摘要hash(content)进行签名。而IOTA 同样是使用上述流程对其发送的消息进行签名(这里主要指交易内容)。因此,我们下 通过以下代码段来看看具体的签名流程Sign(String messageHash,String seed,int index, int security):

    
    {
        //①求私钥key,key.length =  27 * 243 * security
        int[] key = new Signing(curl).key(Converter.trits(seed), index, security);
    
        //②规范化hash摘要,normalizedBundleHash.size = 81 = 27 * 3
        int[] normalizedBundleHash = bundle.normalizedBundle(messageHash);
    
        String[] signeds = new String[security];
        //③ 分段签名,security 为段数
        for (int j =0; j < security; j++) {
          //获取normalizedBundleHash 的第一段内容[0,6561) 
          int[] keyFragment = Arrays.copyOfRange(key, 6561 * j, 6561 * (j + 1));
    
          //获取normalizedBundleHash 的指定段内容[j * 27,27 * (j + 1) - 1] 
          int[] normalizedBundleFragment = Arrays.copyOfRange(normalizedBundleHash, 27 * j, 27 * (j + 1));
    
          //执行指定段签名
          int[] signedFragment = new Signing(curl).signatureFragment(normalizedBundleFragment, keyFragment)
          //签名结果写入signeds
          signeds[j]append = Converter.trytes(signedFragment);
        }
    }
    
    

        我们来解读一下上述代码段:
    ①首先,依据seed、index以及security 求密钥,这里不再深入;
    ②对需要签名的内容messageHash 规范化normalizedBundleHash;
    ③然后依据security 数进行分段签名,并将分段的签名结果写入signeds 中;例如第一段签名signedFragment,则需要依赖私钥key的第一段内容[0,6561) keyFragment 以及 normalizedBundleHash 的第一段内容[0,27)normalizedBundleFragment

        我们先分析规范化normalizedBundle(String bundleHash)干了什么:

      public int[] normalizedBundle(String bundleHash) {
            //防御式判断
            if (bundleHash.length() != 81) {
                throw new RuntimeException("Invalid bundleValidator length: " + bundle.length);
            }
            // 结果存放
            int[] normalizedBundle = new int[81];
            // 具体分成3段处理
            for (int i = 0; i < 3; i++) {
    
                long sum = 0;
                //将Tryte 转成 10进制值,并写入normalizedBundle对应的位置上
                // 求当前段的10进制总和
                for (int j = 0; j < 27; j++) {
                    sum += (normalizedBundle[i * 27 + j] = Converter.value(Converter.trits("" + bundleHash.charAt(i * 27 + j))));
                }
                
                // 对normalizedBundle 求和归0平衡化。
                if (sum >= 0) {
                    while (sum-- > 0) {
                        for (int j = 0; j < 27; j++) {
                            if (normalizedBundle[i * 27 + j] > -13) { //确保不超过下限 -13
                                normalizedBundle[i * 27 + j]--;
                                break;
                            }
                        }
                    }
                } else {
    
                    while (sum++ < 0) {
    
                        for (int j = 0; j < 27; j++) {
    
                            if (normalizedBundle[i * 27 + j] < 13) { // 确保不超过上限13
                                normalizedBundle[i * 27 + j]++;
                                break;
                            }
                        }
                    }
                }
            }
    
            return normalizedBundle;
        }
    

        我们来详细解读上述代码段,根据IOTA 自身的模型设计,其所有的 领域模型(像Bundle、Transaction、Address...)求取其hash 值后,长度都规定为81 Tryte 的字符串,因此,才有防御式判断if (bundleHash.length() != 81);然后,将bundleHash 分三段处理,每段长度为27【[0,27),[27,54),[54,81)】(这里与security[1,2,3]一一对应 )。
        而每段处理的内容如下,首先,将bundleHash(长度81) 转成 10进制int 数组,并将转换结果一一对应写入normalizedBundle(长度81)中。另外,bundleHash是由Tryte 组成,而Tryte 转成10进制的数字的范围为[-13, 13],因此,normalizedBundle 数组中的每一个数的数值范围为[-13, 13],然后在求normalizedBundle当前段的10进制数值总和sum。
        最后,在对normalizedBundle中的每一段做求和归0平衡化,该处理结果后使得normalizedBundle 中的每一段都有一个特性,就是每一段的数值总和为0当该。例如当sum>0时,循环将当前段的第一个10进制数减一,直到修改后sum为0,如果当前段的第一个10进制被减到最小值-13,则从当前段第二个10进制数减继续,一直往后直到sum为0,反之当sum小于0时亦然。
        而归0平衡的目的是化修正hash次数的偏差,这里我认为这种修正基本影响不大,后续的分析读者就会清晰为什么。

        normalizedBundle分析完后,我们接着看看具体的签名实现signatureFragment(normalizedBundleFragment, keyFragment):

        public int[] signatureFragment(int[] normalizedBundleFragment, int[] keyFragment) {
            // 27 * 243,一共27小段
            int[] signatureFragment = keyFragment.clone();
            
            //对27 小段反复hash自身 
            for (int i = 0; i < 27; i++) {
                // 每段hash的次数13 - normalizedBundleFragment[i]
                for (int j = 0; j < 13 - normalizedBundleFragment[i]; j++) {
                    curl.reset()
                            .absorb(signatureFragment, i * HASH_LENGTH, HASH_LENGTH)
                            .squeeze(signatureFragment, i * HASH_LENGTH, HASH_LENGTH);
                }
            }
    
            return signatureFragment;
        }
    

        上述代码段是不是比较眼熟,细心的读者会发现,它与 【2.2.3地址生成】中,通过private key 获取二次摘要的实现基本一致,只不过二次摘要获取实现过程中,对相应私钥小段反复hash 26 次,而签名时,则反复hash (13 - normalizedBundleFragment[i] )次,从而得到具体签名段signatureFragment。具体见下:

    compare

        到这里,签名分析完毕。

    2.2.5ISS验签

        分析完签名后,我们继续分析验签流程Verify(...):

    boolean Verify(String messageHash, String[] signatureFragments,String address) {
            int[][] normalizedBundleFragments = new int[3][27];
            
            int[] normalizedBundleHash = normalizedBundle(messageHash);
    
            // 将normalizedBundleHash 分成3段,与签名时一致每段大小为27
            for (int i = 0; i < 3; i++) {
                normalizedBundleFragments[i] = Arrays.copyOfRange(normalizedBundleHash, i * 27, (i + 1) * 27);
            }
            
            // 签名的段数与security一致
            int security = signatureFragments.length;
            //digests 用于转成地址
            int[] digests = new int[security * 243];
          
           // 通过 签名内容求digest
           for (int i = 0; i < security; i++) {
                //求摘要
                int[] digestBuffer = digest(normalizedBundleFragments[i], Converter.trits(signatureFragments[i]));
    
                System.arraycopy(digestBuffer, 0, digests, i * 243, 243);
            } // end for
    
            // 摘要转地址
            String signatureAddress = Converter.trytes(address(digests))
        
        //比较验签过程所求地址signatureAddress与 实际地址address
        // 若相等则说明验签成功,消息没有被篡改
        return address.equals(signatureAddress);
    }
    
        //对验签段 继续求剩余的hash
        public int[] digest(int[] normalizedBundleFragment, int[] signatureFragment) {
            curl.reset();
            ICurl jCurl = this.getICurlObject(SpongeFactory.Mode.KERL);
            int[] buffer = new int[243];
    
            for (int i = 0; i < 27; i++) {
                buffer = Arrays.copyOfRange(signatureFragment, i * HASH_LENGTH, (i + 1) * 243);
                //
                for (int j = normalizedBundleFragment[i] + 13; j-- > 0; ) {
                    jCurl.reset();
                    jCurl.absorb(buffer);
                    jCurl.squeeze(buffer);
                }
                curl.absorb(buffer);
            }
            curl.squeeze(buffer);
    
            return buffer;
        }
        
        // 通过摘要求地址
        public int[] address(int[] digests) {
            int[] address = new int[HASH_LENGTH];
            ICurl curl = this.getICurlObject(SpongeFactory.Mode.KERL);
            curl.reset()
                    .absorb(digests)
                    .squeeze(address);
            return address;
        }
    

        上述为验签流程的具体实现,我们来详细分析。
        
        首先,对签名内容messageHash先求其normalizedBundle(messageHash),如果messageHash与签名前一致,即消息在传输过程中没有被修改,其normalizedBundleHash 与签名时一致,对于normalizedBundle(...)的实现,上面已分析,这里不再深入;在将normalizedBundleHash 分成三段,写入normalizedBundleFragments中。
        接着,通过for (int i = 0; i < security; i++)循环来来逐段处理验签段signatureFragment,处理方式为通过digest(normalizedBundleFragments[i], Converter.trits(signatureFragment)),继续对签名段中signatureFragment的每一小段(27 * 243, 每小段为243)进行反复自身hash指定次数。我们在仔细比较一下【签名阶段中的degist(...) 】以及 【验签阶段中的degist(...) 】,唯一的区别是,前者的 每一小段 的hash 次数为13 - normalizedBundleFragment[i], 后者是在前者基础上在继续hash 13 + normalizedBundleFragment[i] + 1,即一共27次,这样一来,验签过程degist完成后,其效果不就等价于【2.2.3地址生成】中,通过private key 获取二次摘要的流程,如果验签内容保持不变的情况下,通过验签过程后的degists,在通过Converter.trytes(address(digests)) 转换成signatureAddress,实际上会与消息的发起方address一致,从而达到验签目的。具体效果见下:

        因此,总结一下,将 【地址生成】流程在二次摘要阶段拆分成两段,上部分签名,下部分为验签。而【签名内容】实际就是拆分二次摘要过程中,对私钥段hash 次数的核心。到这里,验签分析完毕。


    2.3 WOTS使用

    2.3.1 WOTS私钥段暴露

        在【LOTS原理】中由详细分析到,LOTS 在每次签名过程中,都有50% 的概率随机暴露接近一半的私钥段,从而导致其私钥不能重用。而WOTS 的签名又是如何暴露私钥段的?细心的读者会发现,签名过程中signatureFragment(normalizedBundleFragment, keyFragment)实现流程中,会对私钥段keyFragment进行指定次数13 - normalizedBundleFragment[i] 的反复hash,而normalizedBundleFragment[i]则是签名内容的tryte hash 摘要 10进制平衡转换,当normalizedBundleFragment[i]为13时(对应tryte 字母M),即13 - normalizedBundleFragment[i] 为0,则直接暴露了当前私钥段,但这都不是核心问题,因为私钥段够长,哪怕暴露了好些私钥段,也基本不会影响安全,唯独的一个核心缺陷就是,如果normalized bundlehash第一个Tryte对应的值即为13(对应normalizedBundleFragment[0] 为13),这表示该地址第一块的私钥被签名暴露了,在根据私钥的生成规则,我们就可以反推出完整私钥。:

        我们来看看IOTA 是如何解决这个问题的?核心在bundle hash 的生成:

        public void finalize(ICurl customCurl) {
            ICurl curl;
            int[] normalizedBundleValue;
            int[] hash = new int[243];
            int[] obsoleteTagTrits = new int[81];
            String hashInTrytes;
            boolean valid = true;
            curl = customCurl == null ? SpongeFactory.create(SpongeFactory.Mode.KERL) : customCurl;
            do {
              // 读取bundle中的内容,并转为trits
              for (int i = 0; i < this.getTransactions().size(); i++) {
                  int[] t = Converter.trits(... + this.getTransactions().get(i).getObsoleteTag() + ...);
              }
    
              curl.absorb(t, 0, t.length);
              // 求取bunlde hash
              curl.squeeze(hash, 0, hash.length);
              ...
              hashInTrytes = Converter.trytes(hash);
              // 转成 normalizedBundle
              normalizedBundleValue = normalizedBundle(hashInTrytes);
    
              boolean foundValue = false;
                for (int aNormalizedBundleValue : normalizedBundleValue) {
                    if (aNormalizedBundleValue == 13) {
                        //代码走到这里,说明normalizedBundleValue存在值为13的10进制数值,需要自增第一笔交易的obsoleteTag,并重新计算bundle hash
                        foundValue = true;
                        obsoleteTagTrits = Converter.trits(this.getTransactions().get(0).getObsoleteTag());
                         //obsoleteTagTrits自增一
                        Converter.increment(obsoleteTagTrits, 81) ; 
                   
                        //重新设置新的ObsoleteTag   
                        this.getTransactions().get(0).setObsoleteTag(Converter.trytes(obsoleteTagTrits));
                    }
                }
              
              valid = !foundValue;
    
            } while (!valid);//当valid 为false,说明normalizedBundle 不存在值为13的10进制数值,可以跳出循环,获取bundle hash
    
            ...
        }
    
    

    为了修复这一漏洞,求bundle hash 时,会同时计算bundle hash 的normalizedBundleHash,如果normalizedBundleHash中包含M的话,会将index为0的交易中obsoleteTag字段加一,然后再算一次bundlehash,循环往复直到normalized bundlehash中不包含M为止,这样一来,确保第一次计算签名的请求不会暴露任何私钥段。

        出于hash 签名的特性,重用地址是非常危险的,不过在IOTA系统中,还是有存在地址重用的情况,协调者所使用的签名方案就是可重用地址的Merkle OTS,因为它确实存在这样的需求,得去批准大量的交易以稳定网络,代价是更长的签名,目前社区中也在探讨可重用地址机制的可行性。
        此外,IOTA的快照机制也会导致地址重用,在IOTA中为了节省存储空间,会定期清空Tangle上的交易,只在记录上保留有余额的地址,因为钱包在由其种子派生出的私钥中按index从上往下进行搜索时碰到余额为0的就会停止,所以在每次快照后有必要将index排在前面的余额为空的地址附加到Tangle上,否则就可能会出现地址的重用,这些会在后面的源码分析在详细讲解。
        到这里,ISS 签名全文分析完毕。

    相关文章

      网友评论

          本文标题:IOTA 基石 - ISS 签名算法详解

          本文链接:https://www.haomeiwen.com/subject/xdeaxctx.html