| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /* |
| * Copyright (c) 2013-2024 Regents of the University of California. |
| * |
| * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions). |
| * |
| * ndn-cxx library is free software: you can redistribute it and/or modify it under the |
| * terms of the GNU Lesser General Public License as published by the Free Software |
| * Foundation, either version 3 of the License, or (at your option) any later version. |
| * |
| * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
| * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. |
| * |
| * You should have received copies of the GNU General Public License and GNU Lesser |
| * General Public License along with ndn-cxx, e.g., in COPYING.md file. If not, see |
| * <http://www.gnu.org/licenses/>. |
| * |
| * See AUTHORS.md for complete list of ndn-cxx authors and contributors. |
| */ |
| |
| #include "ndn-cxx/security/validation-policy-signed-interest.hpp" |
| |
| #include "ndn-cxx/security/interest-signer.hpp" |
| #include "ndn-cxx/security/validation-policy-simple-hierarchy.hpp" |
| |
| #include "tests/test-common.hpp" |
| #include "tests/unit/security/validator-fixture.hpp" |
| |
| #include <boost/mp11/list.hpp> |
| |
| namespace ndn::tests { |
| |
| using namespace ndn::security; |
| |
| BOOST_AUTO_TEST_SUITE(Security) |
| |
| struct SignedInterestDefaultOptions |
| { |
| static auto |
| getOptions() |
| { |
| return ValidationPolicySignedInterest::Options{}; |
| } |
| }; |
| |
| template<class ValidationOptions, class InnerPolicy> |
| class SignedInterestPolicyWrapper : public ValidationPolicySignedInterest |
| { |
| public: |
| SignedInterestPolicyWrapper() |
| : ValidationPolicySignedInterest(make_unique<InnerPolicy>(), ValidationOptions::getOptions()) |
| { |
| } |
| }; |
| |
| template<class ValidationOptions = SignedInterestDefaultOptions, |
| class InnerPolicy = ValidationPolicySimpleHierarchy> |
| class ValidationPolicySignedInterestFixture |
| : public HierarchicalValidatorFixture<SignedInterestPolicyWrapper<ValidationOptions, InnerPolicy>> |
| { |
| protected: |
| Interest |
| makeSignedInterest(const Identity& id, |
| uint32_t signingFlags = InterestSigner::WantNonce | InterestSigner::WantTime) |
| { |
| Interest interest(Name(id.getName()).append("CMD")); |
| m_signer.makeSignedInterest(interest, signingByIdentity(id), signingFlags); |
| return interest; |
| } |
| |
| protected: |
| InterestSigner m_signer{this->m_keyChain}; |
| static constexpr uint32_t WantAll = InterestSigner::WantNonce | |
| InterestSigner::WantTime | |
| InterestSigner::WantSeqNum; |
| }; |
| |
| BOOST_FIXTURE_TEST_SUITE(TestValidationPolicySignedInterest, ValidationPolicySignedInterestFixture<>) |
| |
| BOOST_AUTO_TEST_CASE(Basic) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll); |
| VALIDATE_SUCCESS(i1, "Should succeed (within grace period)"); |
| VALIDATE_FAILURE(i1, "Should fail (replay attack)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| |
| advanceClocks(5_ms); |
| auto i2 = makeSignedInterest(identity, WantAll); |
| VALIDATE_SUCCESS(i2, "Should succeed (timestamp and sequence number larger than previous)"); |
| |
| Interest i3(Name(identity.getName()).append("CMD")); |
| m_signer.makeSignedInterest(i3, signingWithSha256()); |
| VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_AUTO_TEST_CASE(DataPassthrough) |
| { |
| Data d1("/Security/ValidatorFixture/Sub1"); |
| m_keyChain.sign(d1); |
| VALIDATE_SUCCESS(d1, "Should succeed (fallback on inner validation policy for data)"); |
| } |
| |
| BOOST_AUTO_TEST_CASE(InnerPolicyReject) |
| { |
| auto i1 = makeSignedInterest(otherIdentity); |
| VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED); |
| } |
| |
| template<ssize_t count> |
| struct MaxRecordCount |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.timestampGracePeriod = 15_s; |
| options.maxRecordCount = count; |
| return options; |
| } |
| }; |
| |
| BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<3>>) |
| { |
| Identity id1 = addSubCertificate("/Security/ValidatorFixture/Sub1", identity); |
| cache.insert(id1.getDefaultKey().getDefaultCertificate()); |
| Identity id2 = addSubCertificate("/Security/ValidatorFixture/Sub2", identity); |
| cache.insert(id2.getDefaultKey().getDefaultCertificate()); |
| Identity id3 = addSubCertificate("/Security/ValidatorFixture/Sub3", identity); |
| cache.insert(id3.getDefaultKey().getDefaultCertificate()); |
| Identity id4 = addSubCertificate("/Security/ValidatorFixture/Sub4", identity); |
| cache.insert(id4.getDefaultKey().getDefaultCertificate()); |
| |
| auto i1 = makeSignedInterest(id2); |
| auto i2 = makeSignedInterest(id3); |
| auto i3 = makeSignedInterest(id4); |
| auto i00 = makeSignedInterest(id1); // signed at 0s |
| advanceClocks(1_s); |
| auto i01 = makeSignedInterest(id1); // signed at 1s |
| advanceClocks(1_s); |
| auto i02 = makeSignedInterest(id1); // signed at 2s |
| |
| VALIDATE_SUCCESS(i00, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i02, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i3, "Should succeed, forgets identity id1"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i01, "Should succeed despite timestamp is reordered, because record has been evicted"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<-1>>) |
| { |
| std::vector<Identity> identities; |
| for (size_t i = 0; i < 20; ++i) { |
| Identity id = addSubCertificate("/Security/ValidatorFixture/Sub" + to_string(i), identity); |
| cache.insert(id.getDefaultKey().getDefaultCertificate()); |
| identities.push_back(id); |
| } |
| |
| auto i1 = makeSignedInterest(identities.at(0)); // signed at 0s |
| advanceClocks(1_s); |
| for (size_t i = 0; i < 20; ++i) { |
| auto i2 = makeSignedInterest(identities.at(i)); // signed at +1s |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| rewindClockAfterValidation(); |
| } |
| |
| VALIDATE_FAILURE(i1, "Should fail (timestamp reorder)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(ZeroRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<0>>) |
| { |
| auto i1 = makeSignedInterest(identity); // signed at 0s |
| advanceClocks(1_s); |
| auto i2 = makeSignedInterest(identity); // signed at +1s |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| VALIDATE_SUCCESS(i1, "Should succeed despite timestamp reordering, as records aren't kept"); |
| } |
| |
| struct DisabledTimestampValidation |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.shouldValidateTimestamps = false; |
| return options; |
| } |
| }; |
| |
| template<int secs> |
| struct GracePeriodSeconds |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.timestampGracePeriod = time::seconds(secs); |
| return options; |
| } |
| }; |
| |
| BOOST_AUTO_TEST_SUITE(TimestampValidation) |
| |
| BOOST_AUTO_TEST_CASE(MissingTimestamp) |
| { |
| auto i1 = makeSignedInterest(identity, InterestSigner::WantSeqNum); |
| VALIDATE_FAILURE(i1, "Should fail (timestamp missing)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(Disabled, ValidationPolicySignedInterestFixture<DisabledTimestampValidation>) |
| { |
| auto i1 = makeSignedInterest(identity); // signed at 0ms |
| advanceClocks(100_ms); |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity); // signed at +100ms |
| // Set i2 to have same timestamp as i1 |
| auto si2 = i2.getSignatureInfo(); |
| si2->setTime(i2.getSignatureInfo()->getTime()); |
| i2.setSignatureInfo(*si2); |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(TimestampTooOld, ValidationPolicySignedInterestFixture<GracePeriodSeconds<15>>) |
| { |
| auto i1 = makeSignedInterest(identity); // signed at 0s |
| advanceClocks(16_s); // verifying at +16s |
| VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| rewindClockAfterValidation(); |
| |
| auto i2 = makeSignedInterest(identity); // signed at +16s |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(TimestampTooNew, ValidationPolicySignedInterestFixture<GracePeriodSeconds<15>>) |
| { |
| auto i1 = makeSignedInterest(identity); // signed at 0s |
| advanceClocks(1_s); |
| auto i2 = makeSignedInterest(identity); // signed at +1s |
| advanceClocks(1_s); |
| auto i3 = makeSignedInterest(identity); // signed at +2s |
| |
| m_systemClock->advance(-18_s); // verifying at -16s |
| VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| rewindClockAfterValidation(); |
| |
| // SignedInterestValidator should not remember i1's timestamp |
| VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| rewindClockAfterValidation(); |
| |
| // SignedInterestValidator should not remember i2's timestamp, and should treat i3 as initial |
| advanceClocks(18_s); // verifying at +2s |
| VALIDATE_SUCCESS(i3, "Should succeed"); |
| } |
| |
| BOOST_AUTO_TEST_CASE(TimestampReorderEqual) |
| { |
| auto i1 = makeSignedInterest(identity); // signed at 0s |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity); // signed at 0s |
| auto si2 = i2.getSignatureInfo(); |
| si2->setTime(i1.getSignatureInfo()->getTime()); |
| i2.setSignatureInfo(*si2); |
| VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| |
| advanceClocks(2_s); |
| auto i3 = makeSignedInterest(identity); // signed at +2s |
| VALIDATE_SUCCESS(i3, "Should succeed"); |
| } |
| |
| BOOST_AUTO_TEST_CASE(TimestampReorderNegative) |
| { |
| auto i2 = makeSignedInterest(identity); // signed at 0ms |
| advanceClocks(200_ms); |
| auto i3 = makeSignedInterest(identity); // signed at +200ms |
| advanceClocks(900_ms); |
| auto i1 = makeSignedInterest(identity); // signed at +1100ms |
| advanceClocks(300_ms); |
| auto i4 = makeSignedInterest(identity); // signed at +1400ms |
| |
| m_systemClock->advance(-300_ms); // verifying at +1100ms |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| rewindClockAfterValidation(); |
| |
| m_systemClock->advance(-1100_ms); // verifying at 0ms |
| VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| rewindClockAfterValidation(); |
| |
| // SignedInterestValidator should not remember i2's timestamp |
| advanceClocks(200_ms); // verifying at +200ms |
| VALIDATE_FAILURE(i3, "Should fail (timestamp reordered)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| rewindClockAfterValidation(); |
| |
| advanceClocks(1200_ms); // verifying at 1400ms |
| VALIDATE_SUCCESS(i4, "Should succeed"); |
| } |
| |
| using NonPositiveGracePeriods = boost::mp11::mp_list<GracePeriodSeconds<0>, GracePeriodSeconds<-1>>; |
| |
| BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, NonPositiveGracePeriods, |
| ValidationPolicySignedInterestFixture<GracePeriod>) |
| { |
| auto i1 = this->makeSignedInterest(this->identity); // signed at 0ms |
| auto i2 = this->makeSignedInterest(this->subIdentity); // signed at 0ms |
| // ensure timestamps are exactly 0ms |
| for (auto interest : {&i1, &i2}) { |
| auto si = interest->getSignatureInfo(); |
| si->setTime(time::system_clock::now()); |
| interest->setSignatureInfo(*si); |
| } |
| |
| VALIDATE_SUCCESS(i1, "Should succeed when validating at 0ms"); |
| this->rewindClockAfterValidation(); |
| |
| this->advanceClocks(1_ms); |
| VALIDATE_FAILURE(i2, "Should fail when validating at 1ms"); |
| BOOST_TEST(this->lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // TimestampValidation |
| |
| struct EnabledSeqNumValidation |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.shouldValidateSeqNums = true; |
| return options; |
| } |
| }; |
| |
| BOOST_AUTO_TEST_SUITE(SeqNumValidation) |
| |
| // By default, sequence number validation is disabled |
| BOOST_AUTO_TEST_CASE(Disabled) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll); // signed at 0ms |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity, WantAll); // signed at +100ms |
| // Set i2 to have same seq num as i1 |
| auto si2 = i2.getSignatureInfo(); |
| si2->setSeqNum(i2.getSignatureInfo()->getSeqNum()); |
| i2.setSignatureInfo(*si2); |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(MissingSeqNum, ValidationPolicySignedInterestFixture<EnabledSeqNumValidation>) |
| { |
| auto i1 = makeSignedInterest(identity, InterestSigner::WantTime); |
| VALIDATE_FAILURE(i1, "Should fail (sequence number missing"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(SeqNumReorder, ValidationPolicySignedInterestFixture<EnabledSeqNumValidation>) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll); // seq num is i |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity, WantAll); // seq num is i+1 |
| auto si2 = i2.getSignatureInfo(); |
| si2->setSeqNum(i1.getSignatureInfo()->getSeqNum()); |
| i2.setSignatureInfo(*si2); |
| VALIDATE_FAILURE(i2, "Should fail (sequence number reordered)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| |
| auto i3 = makeSignedInterest(identity, WantAll); // seq num is i+2 |
| VALIDATE_SUCCESS(i3, "Should succeed"); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // SeqNumValidation |
| |
| struct DisabledNonceValidation |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.shouldValidateNonces = false; |
| return options; |
| } |
| }; |
| |
| template<ssize_t count> |
| struct MaxNonceRecordCount |
| { |
| static auto |
| getOptions() |
| { |
| ValidationPolicySignedInterest::Options options; |
| options.shouldValidateTimestamps = false; |
| options.shouldValidateSeqNums = false; |
| options.maxNonceRecordCount = count; |
| return options; |
| } |
| }; |
| |
| BOOST_AUTO_TEST_SUITE(NonceValidation) |
| |
| BOOST_AUTO_TEST_CASE(MissingNonce) |
| { |
| auto i1 = makeSignedInterest(identity, InterestSigner::WantTime); // Specifically exclude nonce |
| VALIDATE_FAILURE(i1, "Should fail (nonce missing)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| } |
| |
| BOOST_AUTO_TEST_CASE(DuplicateNonce) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll); |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity, WantAll); |
| auto si2 = i2.getSignatureInfo(); |
| si2->setNonce(i1.getSignatureInfo()->getNonce()); |
| i2.setSignatureInfo(*si2); |
| VALIDATE_FAILURE(i2, "Should fail (duplicate nonce)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| |
| auto i3 = makeSignedInterest(identity, WantAll); |
| // On the off chance that the generated nonce is identical to i1 |
| while (i3.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce()) { |
| i3 = makeSignedInterest(identity, WantAll); |
| } |
| VALIDATE_SUCCESS(i3, "Should succeed"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(Disabled, ValidationPolicySignedInterestFixture<DisabledNonceValidation>) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll ^ InterestSigner::WantNonce); |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| // Ensure still works when a nonce is present |
| auto i2 = makeSignedInterest(identity, WantAll); |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| |
| // Ensure a duplicate still succeeds |
| auto i3 = makeSignedInterest(identity, WantAll); |
| auto si3 = i3.getSignatureInfo(); |
| si3->setNonce(i2.getSignatureInfo()->getNonce()); |
| i3.setSignatureInfo(*si3); |
| m_keyChain.sign(i3, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03) |
| .setSignatureInfo(*si3)); |
| VALIDATE_SUCCESS(i3, "Should succeed"); |
| } |
| |
| BOOST_FIXTURE_TEST_CASE(NonceRecordLimit, ValidationPolicySignedInterestFixture<MaxNonceRecordCount<2>>) |
| { |
| auto i1 = makeSignedInterest(identity, WantAll); |
| VALIDATE_SUCCESS(i1, "Should succeed"); |
| |
| auto i2 = makeSignedInterest(identity, WantAll); |
| // On the off chance that the generated nonce is identical to i1 |
| while (i2.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce()) { |
| i2 = makeSignedInterest(identity, WantAll); |
| } |
| VALIDATE_SUCCESS(i2, "Should succeed"); |
| |
| auto i3 = makeSignedInterest(identity, WantAll); |
| auto si3 = i3.getSignatureInfo(); |
| si3->setNonce(i1.getSignatureInfo()->getNonce()); |
| i3.setSignatureInfo(*si3); |
| m_keyChain.sign(i3, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03) |
| .setSignatureInfo(*si3)); |
| VALIDATE_FAILURE(i3, "Should fail (duplicate nonce)"); |
| BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR); |
| |
| // Pop i1's nonce off the list |
| auto i4 = makeSignedInterest(identity, WantAll); |
| // On the off chance that the generated nonce is identical to i1 or i2 |
| while (i4.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce() || |
| i4.getSignatureInfo()->getNonce() == i2.getSignatureInfo()->getNonce()) { |
| i4 = makeSignedInterest(identity, WantAll); |
| } |
| VALIDATE_SUCCESS(i4, "Should succeed"); |
| |
| // Now i3 should succeed because i1's nonce has been popped off the list |
| auto i5 = makeSignedInterest(identity, WantAll); |
| auto si5 = i5.getSignatureInfo(); |
| si5->setNonce(i1.getSignatureInfo()->getNonce()); |
| i5.setSignatureInfo(*si5); |
| m_keyChain.sign(i5, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03) |
| .setSignatureInfo(*si5)); |
| VALIDATE_SUCCESS(i5, "Should succeed"); |
| } |
| |
| BOOST_AUTO_TEST_SUITE_END() // NonceValidation |
| |
| BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicySignedInterest |
| BOOST_AUTO_TEST_SUITE_END() // Security |
| |
| } // namespace ndn::tests |