示例#1
0
        private static void CheckOracleSecretBag(
            Pkcs12SafeBag safeBag,
            string key,
            string value,
            string keyId)
        {
            Pkcs12SecretBag secretBag = Assert.IsType <Pkcs12SecretBag>(safeBag);

            Assert.Equal("1.2.840.113549.1.16.12.12", secretBag.GetSecretType().Value);

            Assert.Equal(
                MakeOracleKeyValuePairHex(key, value),
                secretBag.SecretValue.ByteArrayToHex());

            CryptographicAttributeObjectCollection attrs = secretBag.Attributes;

            Assert.Equal(1, attrs.Count);
            CryptographicAttributeObject firstAttr = attrs[0];

            Assert.Equal(Oids.LocalKeyId, firstAttr.Oid.Value);
            Assert.Equal(1, firstAttr.Values.Count);
            Pkcs9LocalKeyId localKeyId = Assert.IsType <Pkcs9LocalKeyId>(firstAttr.Values[0]);

            Assert.Equal(keyId, localKeyId.KeyId.ByteArrayToHex());
        }
示例#2
0
        public static void KeyIdFromRawData(string inputHex, string expectedHex)
        {
            var             attr       = new Pkcs9AttributeObject(Oids.LocalKeyId, inputHex.HexToByteArray());
            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId();

            localKeyId.CopyFrom(attr);

            Assert.Equal(expectedHex, localKeyId.KeyId.ByteArrayToHex());
        }
示例#3
0
        public static void KeyIdFromInvalidData(string invalidHex)
        {
            var             attr       = new Pkcs9AttributeObject(Oids.LocalKeyId, invalidHex.HexToByteArray());
            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId();

            localKeyId.CopyFrom(attr);

            Assert.ThrowsAny <CryptographicException>(() => localKeyId.KeyId);
        }
示例#4
0
        public static void WriteOneCertWithKey_LikeWindows()
        {
            Pkcs12SafeContents safe1 = new Pkcs12SafeContents();
            Pkcs12SafeContents safe2 = new Pkcs12SafeContents();

            byte[] rawData;

            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId(new byte[] { 1 });
            const string    password   = nameof(WriteOneCertWithKey_LikeWindows);

            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey(true))
            {
                Pkcs12CertBag certBag = safe1.AddCertificate(cert);
                certBag.Attributes.Add(localKeyId);

                rawData = cert.RawData;

                Pkcs12ShroudedKeyBag keyBag;

                using (RSA rsa = cert.GetRSAPrivateKey())
                {
                    keyBag = safe2.AddShroudedKey(
                        rsa,
                        password,
                        s_win7Pbe);
                }

                keyBag.Attributes.Add(localKeyId);
            }

            Pkcs12Builder builder = new Pkcs12Builder();

            builder.AddSafeContentsEncrypted(
                safe1,
                password,
                s_win7Pbe);

            builder.AddSafeContentsUnencrypted(safe2);

            builder.SealWithMac(password, HashAlgorithmName.SHA1, 2068);
            byte[] pfx = builder.Encode();

            ImportedCollection coll =
                ImportedCollection.Import(pfx, password, X509KeyStorageFlags.EphemeralKeySet);

            using (coll)
            {
                Assert.Equal(1, coll.Collection.Count);
                Assert.Equal(rawData, coll.Collection[0].RawData);
                Assert.True(coll.Collection[0].HasPrivateKey, "coll.Collection[0].HasPrivateKey");
            }
        }
示例#5
0
        public static void KeyIdCtorAcceptsEmpty()
        {
            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId(ReadOnlySpan <byte> .Empty);

            Assert.Equal(0, localKeyId.KeyId.Length);

            Oid oid = localKeyId.Oid;

            Assert.NotNull(oid);
            Assert.Equal(Oids.LocalKeyId, oid.Value);

            Assert.Equal("0400", localKeyId.RawData.ByteArrayToHex());
        }
示例#6
0
        public static void DefaultCtor()
        {
            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId();

            Assert.Equal(0, localKeyId.KeyId.Length);

            Oid oid = localKeyId.Oid;

            Assert.NotNull(oid);
            Assert.Equal(Oids.LocalKeyId, oid.Value);

            Assert.Null(localKeyId.RawData);
        }
示例#7
0
        public static void DefaultCtor()
        {
            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId();

            Assert.Throws <CryptographicException>(() => localKeyId.KeyId);

            Oid oid = localKeyId.Oid;

            Assert.NotNull(oid);
            Assert.Equal(Oids.LocalKeyId, oid.Value);

            Assert.Empty(localKeyId.RawData);
        }
示例#8
0
        public void CertAndKeyTwice(bool addLocalKeyId, bool crossIdentifiers)
        {
            string pw = nameof(CertAndKeyTwice);

            using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags))
                using (RSA key = cert.GetRSAPrivateKey())
                {
                    Pkcs12Builder      builder      = new Pkcs12Builder();
                    Pkcs12SafeContents keyContents  = new Pkcs12SafeContents();
                    Pkcs12SafeContents certContents = new Pkcs12SafeContents();

                    Pkcs12SafeBag key1  = keyContents.AddShroudedKey(key, pw, s_windowsPbe);
                    Pkcs12SafeBag key2  = keyContents.AddShroudedKey(key, pw, s_windowsPbe);
                    Pkcs12SafeBag cert1 = certContents.AddCertificate(cert);
                    Pkcs12SafeBag cert2 = certContents.AddCertificate(cert);

                    if (addLocalKeyId)
                    {
                        (crossIdentifiers ? key2 : key1).Attributes.Add(s_keyIdOne);
                        cert1.Attributes.Add(s_keyIdOne);

                        Pkcs9LocalKeyId id2 = new Pkcs9LocalKeyId(cert.GetCertHash());
                        (crossIdentifiers ? key1 : key2).Attributes.Add(id2);
                        cert2.Attributes.Add(id2);
                    }

                    AddContents(keyContents, builder, pw, encrypt: false);
                    AddContents(certContents, builder, pw, encrypt: true);
                    builder.SealWithMac(pw, s_digestAlgorithm, MacCount);
                    byte[] pfxBytes = builder.Encode();

                    if (addLocalKeyId)
                    {
                        ReadMultiPfx(
                            pfxBytes,
                            pw,
                            cert,
                            new[] { cert, cert },
                            CheckKeyConsistency);
                    }
                    else
                    {
                        ReadUnreadablePfx(
                            pfxBytes,
                            pw,
                            // NTE_BAD_DATA
                            -2146893819);
                    }
                }
        }
示例#9
0
        public static void KeyIdCtorPreservesValue()
        {
            byte[] keyId    = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };
            string keyIdHex = keyId.ByteArrayToHex();

            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId(keyId);

            Assert.Equal(keyIdHex, localKeyId.KeyId.ByteArrayToHex());

            Oid oid = localKeyId.Oid;

            Assert.NotNull(oid);
            Assert.Equal(Oids.LocalKeyId, oid.Value);

            Assert.Equal(
                $"04{keyId.Length:X2}{keyIdHex}",
                localKeyId.RawData.ByteArrayToHex());
        }
示例#10
0
        public static void WriteOneCertWithKey_Encrypted_SameSafe()
        {
            Pkcs12SafeContents contents = new Pkcs12SafeContents();

            byte[] rawData;

            Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId(new byte[] { 1 });

            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey(true))
                using (RSA certKey = cert.GetRSAPrivateKey())
                    using (RSA exportableKey = certKey.MakeExportable())
                    {
                        Pkcs12CertBag certBag = contents.AddCertificate(cert);
                        certBag.Attributes.Add(localKeyId);

                        rawData = cert.RawData;

                        Pkcs12KeyBag keyBag = contents.AddKeyUnencrypted(exportableKey);
                        keyBag.Attributes.Add(localKeyId);
                    }

            const string password = nameof(WriteOneCertWithKey_Encrypted_SameSafe);

            Pkcs12Builder builder = new Pkcs12Builder();

            builder.AddSafeContentsEncrypted(
                contents,
                password,
                s_win7Pbe);

            builder.SealWithMac(password, HashAlgorithmName.SHA1, 1024);
            byte[] pfx = builder.Encode();

            ImportedCollection coll =
                ImportedCollection.Import(pfx, password, X509KeyStorageFlags.EphemeralKeySet);

            using (coll)
            {
                Assert.Equal(1, coll.Collection.Count);
                Assert.Equal(rawData, coll.Collection[0].RawData);
                Assert.True(coll.Collection[0].HasPrivateKey, "coll.Collection[0].HasPrivateKey");
            }
        }
示例#11
0
        public void CertAndKeyTwice_KeysUntagged()
        {
            string pw = nameof(CertAndKeyTwice);

            using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags))
                using (RSA key = cert.GetRSAPrivateKey())
                {
                    Pkcs12Builder      builder      = new Pkcs12Builder();
                    Pkcs12SafeContents keyContents  = new Pkcs12SafeContents();
                    Pkcs12SafeContents certContents = new Pkcs12SafeContents();

                    Pkcs12SafeBag key1  = keyContents.AddShroudedKey(key, pw, s_windowsPbe);
                    Pkcs12SafeBag key2  = keyContents.AddShroudedKey(key, pw, s_windowsPbe);
                    Pkcs12SafeBag cert1 = certContents.AddCertificate(cert);
                    Pkcs12SafeBag cert2 = certContents.AddCertificate(cert);

                    Pkcs9LocalKeyId id2 = new Pkcs9LocalKeyId(cert.GetCertHash());
                    Pkcs9LocalKeyId id3 = new Pkcs9LocalKeyId(BitConverter.GetBytes(3));
                    Pkcs9LocalKeyId id4 = new Pkcs9LocalKeyId(BitConverter.GetBytes(4));
                    cert1.Attributes.Add(s_keyIdOne);
                    cert2.Attributes.Add(id2);
                    key1.Attributes.Add(id3);
                    key2.Attributes.Add(id4);

                    AddContents(keyContents, builder, pw, encrypt: false);
                    AddContents(certContents, builder, pw, encrypt: true);
                    builder.SealWithMac(pw, s_digestAlgorithm, MacCount);
                    byte[] pfxBytes = builder.Encode();

                    ReadUnreadablePfx(
                        pfxBytes,
                        pw,
                        // NTE_BAD_DATA
                        -2146893819);
                }
        }
示例#12
0
        public static void ReadIndefiniteEncodingNoMac(int trailingByteCount)
        {
            ReadOnlyMemory <byte> source = PadContents(Pkcs12Documents.IndefiniteEncodingNoMac, trailingByteCount);

            Pkcs12Info info = Pkcs12Info.Decode(
                source,
                out int bytesRead,
                skipCopy: true);

            Assert.Equal(Pkcs12Documents.IndefiniteEncodingNoMac.Length, bytesRead);
            Assert.Equal(Pkcs12IntegrityMode.None, info.IntegrityMode);

            ReadOnlyCollection <Pkcs12SafeContents> safes = info.AuthenticatedSafe;

            Assert.Equal(2, safes.Count);

            Pkcs12SafeContents firstSafe  = safes[0];
            Pkcs12SafeContents secondSafe = safes[1];

            Assert.Equal(Pkcs12ConfidentialityMode.None, firstSafe.ConfidentialityMode);
            Assert.Equal(Pkcs12ConfidentialityMode.None, secondSafe.ConfidentialityMode);

            Assert.True(firstSafe.IsReadOnly, "firstSafe.IsReadOnly");
            Assert.True(secondSafe.IsReadOnly, "secondSafe.IsReadOnly");

            Pkcs12SafeBag[] firstContents  = firstSafe.GetBags().ToArray();
            Pkcs12SafeBag[] secondContents = secondSafe.GetBags().ToArray();

            Assert.Equal(1, firstContents.Length);
            Assert.Equal(1, secondContents.Length);

            Pkcs12KeyBag  keyBag  = Assert.IsType <Pkcs12KeyBag>(firstContents[0]);
            Pkcs12CertBag certBag = Assert.IsType <Pkcs12CertBag>(secondContents[0]);

            CryptographicAttributeObjectCollection keyBagAttrs  = keyBag.Attributes;
            CryptographicAttributeObjectCollection certBagAttrs = certBag.Attributes;

            Assert.Equal(2, keyBagAttrs.Count);
            Assert.Equal(2, certBagAttrs.Count);

            Assert.Equal(Oids.FriendlyName, keyBagAttrs[0].Oid.Value);
            Assert.Equal(1, keyBagAttrs[0].Values.Count);
            Assert.Equal(Oids.LocalKeyId, keyBagAttrs[1].Oid.Value);
            Assert.Equal(1, keyBagAttrs[1].Values.Count);

            Pkcs9AttributeObject keyFriendlyName =
                Assert.IsAssignableFrom <Pkcs9AttributeObject>(keyBagAttrs[0].Values[0]);

            Pkcs9LocalKeyId keyKeyId = Assert.IsType <Pkcs9LocalKeyId>(keyBagAttrs[1].Values[0]);

            Assert.Equal(Oids.FriendlyName, certBagAttrs[0].Oid.Value);
            Assert.Equal(1, certBagAttrs[0].Values.Count);
            Assert.Equal(Oids.LocalKeyId, certBagAttrs[1].Oid.Value);
            Assert.Equal(1, certBagAttrs[1].Values.Count);

            Pkcs9AttributeObject certFriendlyName =
                Assert.IsAssignableFrom <Pkcs9AttributeObject>(certBagAttrs[0].Values[0]);

            Pkcs9LocalKeyId certKeyId = Assert.IsType <Pkcs9LocalKeyId>(certBagAttrs[1].Values[0]);

            // This PFX gave a friendlyName value of "cert" to both the key and the cert.
            Assert.Equal("1E080063006500720074", keyFriendlyName.RawData.ByteArrayToHex());
            Assert.Equal(keyFriendlyName.RawData, certFriendlyName.RawData);

            // The private key (KeyBag) and the public key (CertBag) are matched from their keyId value.
            Assert.Equal("0414EDF3D122CF623CF0CFC9CD226261E8415A83E630", keyKeyId.RawData.ByteArrayToHex());
            Assert.Equal("EDF3D122CF623CF0CFC9CD226261E8415A83E630", keyKeyId.KeyId.ByteArrayToHex());
            Assert.Equal(keyKeyId.RawData, certKeyId.RawData);

            using (X509Certificate2 cert = certBag.GetCertificate())
                using (RSA privateKey = RSA.Create())
                    using (RSA publicKey = cert.GetRSAPublicKey())
                    {
                        privateKey.ImportPkcs8PrivateKey(keyBag.Pkcs8PrivateKey.Span, out _);

                        Assert.Equal(
                            publicKey.ExportSubjectPublicKeyInfo().ByteArrayToHex(),
                            privateKey.ExportSubjectPublicKeyInfo().ByteArrayToHex());
                    }
        }
示例#13
0
        public static void Test1()
        {
            var loader = (CertLoaderFromRawData)Certificates.RSAKeyTransferCapi1;
            ReadOnlyMemory <byte> pfxData = loader.PfxData;

            Pkcs12Info info = Pkcs12Info.Decode(pfxData, out int bytesConsumed);

            Assert.Equal(pfxData.Length, bytesConsumed);

            Assert.Equal(Pkcs12IntegrityMode.Password, info.IntegrityMode);
            CheckMac(info, loader.Password);

            ReadOnlyCollection <Pkcs12SafeContents> authSafe = info.AuthenticatedSafe;

            Assert.Same(authSafe, info.AuthenticatedSafe);
            Assert.Equal(2, authSafe.Count);

            Assert.Equal(Pkcs12ConfidentialityMode.None, authSafe[0].ConfidentialityMode);
            Assert.Equal(Pkcs12ConfidentialityMode.None, authSafe[1].ConfidentialityMode);

            List <Pkcs12SafeBag> safe0Bags = new List <Pkcs12SafeBag>(authSafe[0].GetBags());

            Assert.Equal(1, safe0Bags.Count);
            Pkcs12ShroudedKeyBag shroudedKeyBag = Assert.IsType <Pkcs12ShroudedKeyBag>(safe0Bags[0]);

            CryptographicAttributeObjectCollection keyBagAttrs = shroudedKeyBag.Attributes;

            Assert.Same(keyBagAttrs, shroudedKeyBag.Attributes);
            Assert.Equal(2, keyBagAttrs.Count);
            Assert.Equal(Oids.LocalKeyId, keyBagAttrs[0].Oid.Value);
            Assert.Equal(1, keyBagAttrs[0].Values.Count);
            Pkcs9LocalKeyId keyKeyId = Assert.IsType <Pkcs9LocalKeyId>(keyBagAttrs[0].Values[0]);

            Assert.Equal("1.3.6.1.4.1.311.17.1", keyBagAttrs[1].Oid.Value);
            Assert.Equal(1, keyBagAttrs[1].Values.Count);
            Pkcs9AttributeObject cspNameAttr = Assert.IsType <Pkcs9AttributeObject>(keyBagAttrs[1].Values[0]);

            byte[] cspNameBytes = Encoding.BigEndianUnicode.GetBytes("Microsoft Strong Cryptographic Provider");

            Assert.Equal(
                $"1E{cspNameBytes.Length:X2}{cspNameBytes.ByteArrayToHex()}",
                cspNameAttr.RawData.ByteArrayToHex());

            List <Pkcs12SafeBag> safe1Bags = new List <Pkcs12SafeBag>(authSafe[1].GetBags());

            Assert.Equal(1, safe0Bags.Count);
            Assert.IsType <Pkcs12CertBag>(safe1Bags[0]);
            Pkcs12CertBag certBag = (Pkcs12CertBag)safe1Bags[0];

            Assert.True(certBag.IsX509Certificate, "certBag.IsX509Certificate");
            Assert.InRange(certBag.EncodedCertificate.Length, loader.CerData.Length + 2, int.MaxValue);

            CryptographicAttributeObjectCollection certBagAttrs = certBag.Attributes;

            Assert.Same(certBagAttrs, certBag.Attributes);
            Assert.Equal(1, certBagAttrs.Count);
            Assert.Equal(Oids.LocalKeyId, certBagAttrs[0].Oid.Value);
            Assert.Equal(1, certBagAttrs[0].Values.Count);
            Pkcs9LocalKeyId certKeyId = Assert.IsType <Pkcs9LocalKeyId>(certBagAttrs[0].Values[0]);

            Assert.Equal(keyKeyId.KeyId.ByteArrayToHex(), certKeyId.KeyId.ByteArrayToHex());

            byte[] data = { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
            byte[] encrypted;

            using (X509Certificate2 fromLoader = loader.GetCertificate())
                using (X509Certificate2 fromBag = certBag.GetCertificate())
                    using (RSA loaderPub = fromLoader.GetRSAPublicKey())
                    {
                        Assert.Equal(fromLoader.RawData, fromBag.RawData);

                        encrypted = loaderPub.Encrypt(data, RSAEncryptionPadding.OaepSHA1);
                    }

            int bytesRead;

            using (RSA rsa = RSA.Create())
            {
                rsa.ImportEncryptedPkcs8PrivateKey(
                    loader.Password,
                    shroudedKeyBag.EncryptedPkcs8PrivateKey.Span,
                    out bytesRead);

                byte[] dec = rsa.Decrypt(encrypted, RSAEncryptionPadding.OaepSHA1);
                Assert.Equal(data, dec);
            }

            Assert.Equal(shroudedKeyBag.EncryptedPkcs8PrivateKey.Length, bytesRead);
        }
示例#14
0
        public void TwoCerts_TwoKeys_ManySafeContentsValues(bool invertCertOrder, bool invertKeyOrder)
        {
            string pw = nameof(TwoCerts_TwoKeys_ManySafeContentsValues);

            using (ImportedCollection ic = Cert.Import(TestData.MultiPrivateKeyPfx, null, s_exportableImportFlags))
            {
                X509Certificate2Collection certs  = ic.Collection;
                X509Certificate2           first  = certs[0];
                X509Certificate2           second = certs[1];

                if (invertCertOrder)
                {
                    X509Certificate2 tmp = first;
                    first  = second;
                    second = tmp;
                }

                using (AsymmetricAlgorithm firstKey = first.GetRSAPrivateKey())
                    using (AsymmetricAlgorithm secondKey = second.GetRSAPrivateKey())
                    {
                        AsymmetricAlgorithm firstAdd  = firstKey;
                        AsymmetricAlgorithm secondAdd = secondKey;

                        if (invertKeyOrder != invertCertOrder)
                        {
                            AsymmetricAlgorithm tmp = firstKey;
                            firstAdd  = secondAdd;
                            secondAdd = tmp;
                        }

                        Pkcs12Builder      builder            = new Pkcs12Builder();
                        Pkcs12SafeContents firstKeyContents   = new Pkcs12SafeContents();
                        Pkcs12SafeContents secondKeyContents  = new Pkcs12SafeContents();
                        Pkcs12SafeContents firstCertContents  = new Pkcs12SafeContents();
                        Pkcs12SafeContents secondCertContents = new Pkcs12SafeContents();

                        Pkcs12SafeContents irrelevant = new Pkcs12SafeContents();
                        irrelevant.AddSecret(new Oid("0.0"), new byte[] { 0x05, 0x00 });

                        Pkcs12SafeBag firstAddedKeyBag  = firstKeyContents.AddShroudedKey(firstAdd, pw, s_windowsPbe);
                        Pkcs12SafeBag secondAddedKeyBag = secondKeyContents.AddShroudedKey(secondAdd, pw, s_windowsPbe);
                        Pkcs12SafeBag firstCertBag      = firstCertContents.AddCertificate(first);
                        Pkcs12SafeBag secondCertBag     = secondCertContents.AddCertificate(second);
                        Pkcs12SafeBag firstKeyBag       = firstAddedKeyBag;
                        Pkcs12SafeBag secondKeyBag      = secondAddedKeyBag;

                        if (invertKeyOrder != invertCertOrder)
                        {
                            Pkcs12SafeBag tmp = firstKeyBag;
                            firstKeyBag  = secondKeyBag;
                            secondKeyBag = tmp;
                        }

                        firstCertBag.Attributes.Add(s_keyIdOne);
                        firstKeyBag.Attributes.Add(s_keyIdOne);

                        Pkcs9LocalKeyId secondKeyId = new Pkcs9LocalKeyId(second.GetCertHash());
                        secondCertBag.Attributes.Add(secondKeyId);
                        secondKeyBag.Attributes.Add(secondKeyId);

                        // 2C, 1K, 1C, 2K
                        // With some non-participating contents values sprinkled in for good measure.
                        AddContents(irrelevant, builder, pw, encrypt: true);
                        AddContents(secondCertContents, builder, pw, encrypt: true);
                        AddContents(irrelevant, builder, pw, encrypt: false);
                        AddContents(firstKeyContents, builder, pw, encrypt: false);
                        AddContents(firstCertContents, builder, pw, encrypt: true);
                        AddContents(irrelevant, builder, pw, encrypt: false);
                        AddContents(secondKeyContents, builder, pw, encrypt: true);
                        AddContents(irrelevant, builder, pw, encrypt: true);

                        builder.SealWithMac(pw, s_digestAlgorithm, MacCount);
                        byte[] pfxBytes = builder.Encode();

                        X509Certificate2[] expectedOrder = { first, second };

                        Action <X509Certificate2> followup = CheckKeyConsistency;

                        // For unknown reasons, CheckKeyConsistency on this test fails
                        // on Windows 7 with an Access Denied in all variations for
                        // Collections, and in invertCertOrder: true for Single.
                        //
                        // Obviously this hit some sort of weird corner case in the Win7
                        // loader, but it's not important to the test.

                        if (OperatingSystem.IsWindows() &&
                            !PlatformDetection.IsWindows8xOrLater)
                        {
                            followup = null;
                        }

                        ReadMultiPfx(
                            pfxBytes,
                            pw,
                            first,
                            expectedOrder,
                            followup);
                    }
            }
        }