diff --git a/contracts/dnsregistrar/DNSClaimChecker.sol b/contracts/dnsregistrar/DNSClaimChecker.sol index 467ee5f9..48e629e0 100644 --- a/contracts/dnsregistrar/DNSClaimChecker.sol +++ b/contracts/dnsregistrar/DNSClaimChecker.sol @@ -1,3 +1,4 @@ +//SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "../dnssec-oracle/DNSSEC.sol"; @@ -13,41 +14,25 @@ library DNSClaimChecker { uint16 constant CLASS_INET = 1; uint16 constant TYPE_TXT = 16; - function getOwnerAddress( - DNSSEC oracle, - bytes memory name, - bytes memory proof - ) internal view returns (address, bool) { + function getOwnerAddress(bytes memory name, bytes memory data) + internal + pure + returns (address, bool) + { // Add "_ens." to the front of the name. Buffer.buffer memory buf; buf.init(name.length + 5); buf.append("\x04_ens"); buf.append(name); - bytes20 hash; - uint32 expiration; - // Check the provided TXT record has been validated by the oracle - (, expiration, hash) = oracle.rrdata(TYPE_TXT, buf.buf); - if (hash == bytes20(0) && proof.length == 0) - return (address(0x0), false); - - require(hash == bytes20(keccak256(proof))); for ( - RRUtils.RRIterator memory iter = proof.iterateRRs(0); + RRUtils.RRIterator memory iter = data.iterateRRs(0); !iter.done(); iter.next() ) { - require( - RRUtils.serialNumberGte( - expiration + iter.ttl, - uint32(block.timestamp) - ), - "DNS record is stale; refresh or delete it before proceeding." - ); - bool found; address addr; - (addr, found) = parseRR(proof, iter.rdataOffset); + (addr, found) = parseRR(data, iter.rdataOffset); if (found) { return (addr, true); } diff --git a/contracts/dnsregistrar/DNSRegistrar.sol b/contracts/dnsregistrar/DNSRegistrar.sol index ca5b5a30..ed6f2191 100644 --- a/contracts/dnsregistrar/DNSRegistrar.sol +++ b/contracts/dnsregistrar/DNSRegistrar.sol @@ -1,47 +1,51 @@ +//SPDX-License-Identifier: MIT + pragma solidity ^0.8.4; -pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@ensdomains/buffer/contracts/Buffer.sol"; import "../dnssec-oracle/BytesUtils.sol"; import "../dnssec-oracle/DNSSEC.sol"; +import "../dnssec-oracle/RRUtils.sol"; import "../registry/ENSRegistry.sol"; import "../root/Root.sol"; +import "../resolvers/profiles/AddrResolver.sol"; import "./DNSClaimChecker.sol"; import "./PublicSuffixList.sol"; -import "../resolvers/profiles/AddrResolver.sol"; - -interface IDNSRegistrar { - function claim(bytes memory name, bytes memory proof) external; - - function proveAndClaim( - bytes memory name, - DNSSEC.RRSetWithSignature[] memory input, - bytes memory proof - ) external; - - function proveAndClaimWithResolver( - bytes memory name, - DNSSEC.RRSetWithSignature[] memory input, - bytes memory proof, - address resolver, - address addr - ) external; -} +import "./IDNSRegistrar.sol"; /** * @dev An ENS registrar that allows the owner of a DNS name to claim the * corresponding name in ENS. */ -contract DNSRegistrar is IDNSRegistrar { +// TODO: Record inception time of any claimed name, so old proofs can't be used to revert changes to a name. +contract DNSRegistrar is IDNSRegistrar, IERC165 { using BytesUtils for bytes; + using Buffer for Buffer.buffer; + using RRUtils for *; - DNSSEC public oracle; - ENS public ens; + ENS public immutable ens; + DNSSEC public immutable oracle; PublicSuffixList public suffixes; + // A mapping of the most recent signatures seen for each claimed domain. + mapping(bytes32 => uint32) public inceptions; - bytes4 private constant INTERFACE_META_ID = - bytes4(keccak256("supportsInterface(bytes4)")); + error NoOwnerRecordFound(); + error StaleProof(); - event Claim(bytes32 indexed node, address indexed owner, bytes dnsname); + struct OwnerRecord { + bytes name; + address owner; + address resolver; + uint64 ttl; + } + + event Claim( + bytes32 indexed node, + address indexed owner, + bytes dnsname, + uint32 inception + ); event NewOracle(address oracle); event NewPublicSuffixList(address suffixes); @@ -67,60 +71,36 @@ contract DNSRegistrar is IDNSRegistrar { _; } - function setOracle(DNSSEC _dnssec) public onlyOwner { - oracle = _dnssec; - emit NewOracle(address(oracle)); - } - function setPublicSuffixList(PublicSuffixList _suffixes) public onlyOwner { suffixes = _suffixes; emit NewPublicSuffixList(address(suffixes)); } - /** - * @dev Claims a name by proving ownership of its DNS equivalent. - * @param name The name to claim, in DNS wire format. - * @param proof A DNS RRSet proving ownership of the name. Must be verified - * in the DNSSEC oracle before calling. This RRSET must contain a TXT - * record for '_ens.' + name, with the value 'a=0x...'. Ownership of - * the name will be transferred to the address specified in the TXT - * record. - */ - function claim(bytes memory name, bytes memory proof) public override { - (bytes32 rootNode, bytes32 labelHash, address addr) = _claim( - name, - proof - ); - ens.setSubnodeOwner(rootNode, labelHash, addr); - } - /** * @dev Submits proofs to the DNSSEC oracle, then claims a name using those proofs. * @param name The name to claim, in DNS wire format. - * @param input The data to be passed to the Oracle's `submitProofs` function. The last - * proof must be the TXT record required by the registrar. - * @param proof The proof record for the first element in input. + * @param input A chain of signed DNS RRSETs ending with a text record. */ function proveAndClaim( bytes memory name, - DNSSEC.RRSetWithSignature[] memory input, - bytes memory proof + DNSSEC.RRSetWithSignature[] memory input ) public override { - proof = oracle.submitRRSets(input, proof); - claim(name, proof); + (bytes32 rootNode, bytes32 labelHash, address addr) = _claim( + name, + input + ); + ens.setSubnodeOwner(rootNode, labelHash, addr); } function proveAndClaimWithResolver( bytes memory name, DNSSEC.RRSetWithSignature[] memory input, - bytes memory proof, address resolver, address addr ) public override { - proof = oracle.submitRRSets(input, proof); (bytes32 rootNode, bytes32 labelHash, address owner) = _claim( name, - proof + input ); require( msg.sender == owner, @@ -152,21 +132,24 @@ contract DNSRegistrar is IDNSRegistrar { function supportsInterface(bytes4 interfaceID) external pure + override returns (bool) { return - interfaceID == INTERFACE_META_ID || + interfaceID == type(IERC165).interfaceId || interfaceID == type(IDNSRegistrar).interfaceId; } - function _claim(bytes memory name, bytes memory proof) + function _claim(bytes memory name, DNSSEC.RRSetWithSignature[] memory input) internal returns ( - bytes32 rootNode, + bytes32 parentNode, bytes32 labelHash, address addr ) { + (bytes memory data, uint32 inception) = oracle.verifyRRSet(input); + // Get the first label uint256 labelLen = name.readUint8(0); labelHash = name.keccak(1, labelLen); @@ -182,15 +165,17 @@ contract DNSRegistrar is IDNSRegistrar { ); // Make sure the parent name is enabled - rootNode = enableNode(parentName, 0); + parentNode = enableNode(parentName, 0); + + bytes32 node = keccak256(abi.encodePacked(parentNode, labelHash)); + if (!RRUtils.serialNumberGte(inception, inceptions[node])) { + revert StaleProof(); + } + inceptions[node] = inception; - (addr, ) = DNSClaimChecker.getOwnerAddress(oracle, name, proof); + (addr, ) = DNSClaimChecker.getOwnerAddress(name, data); - emit Claim( - keccak256(abi.encodePacked(rootNode, labelHash)), - addr, - name - ); + emit Claim(node, addr, name, inception); } function enableNode(bytes memory domain, uint256 offset) diff --git a/contracts/dnsregistrar/IDNSRegistrar.sol b/contracts/dnsregistrar/IDNSRegistrar.sol new file mode 100644 index 00000000..7963787e --- /dev/null +++ b/contracts/dnsregistrar/IDNSRegistrar.sol @@ -0,0 +1,18 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../dnssec-oracle/DNSSEC.sol"; + +interface IDNSRegistrar { + function proveAndClaim( + bytes memory name, + DNSSEC.RRSetWithSignature[] memory input + ) external; + + function proveAndClaimWithResolver( + bytes memory name, + DNSSEC.RRSetWithSignature[] memory input, + address resolver, + address addr + ) external; +} diff --git a/contracts/dnsregistrar/RecordParser.sol b/contracts/dnsregistrar/RecordParser.sol new file mode 100644 index 00000000..f224efae --- /dev/null +++ b/contracts/dnsregistrar/RecordParser.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.11; + +import "../dnssec-oracle/BytesUtils.sol"; + +library RecordParser { + using BytesUtils for bytes; + + /** + * @dev Parses a key-value record into a key and value. + * @param input The input string + * @param offset The offset to start reading at + */ + function readKeyValue( + bytes memory input, + uint256 offset, + uint256 len + ) + internal + pure + returns ( + bytes memory key, + bytes memory value, + uint256 nextOffset + ) + { + uint256 separator = input.find(offset, len, "="); + if (separator == type(uint256).max) { + return ("", "", type(uint256).max); + } + + uint256 terminator = input.find( + separator, + len + offset - separator, + " " + ); + if (terminator == type(uint256).max) { + terminator = input.length; + } + + key = input.substring(offset, separator - offset); + value = input.substring(separator + 1, terminator - separator - 1); + nextOffset = terminator + 1; + } +} diff --git a/contracts/dnssec-oracle/BytesUtils.sol b/contracts/dnssec-oracle/BytesUtils.sol index a7f84faa..5695a930 100644 --- a/contracts/dnssec-oracle/BytesUtils.sol +++ b/contracts/dnssec-oracle/BytesUtils.sol @@ -377,4 +377,26 @@ library BytesUtils { return bytes32(ret << (256 - bitlen)); } + + /** + * @dev Finds the first occurrence of the byte `needle` in `self`. + * @param self The string to search + * @param off The offset to start searching at + * @param len The number of bytes to search + * @param needle The byte to search for + * @return The offset of `needle` in `self`, or 2**256-1 if it was not found. + */ + function find( + bytes memory self, + uint256 off, + uint256 len, + bytes1 needle + ) internal pure returns (uint256) { + for (uint256 idx = off; idx < off + len; idx++) { + if (self[idx] == needle) { + return idx; + } + } + return type(uint256).max; + } } diff --git a/contracts/dnssec-oracle/DNSSEC.sol b/contracts/dnssec-oracle/DNSSEC.sol index e4bc7def..f24cdb9a 100644 --- a/contracts/dnssec-oracle/DNSSEC.sol +++ b/contracts/dnssec-oracle/DNSSEC.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; pragma experimental ABIEncoderV2; @@ -11,41 +12,16 @@ abstract contract DNSSEC { event AlgorithmUpdated(uint8 id, address addr); event DigestUpdated(uint8 id, address addr); - event NSEC3DigestUpdated(uint8 id, address addr); - event RRSetUpdated(bytes name, bytes rrset); - function submitRRSets(RRSetWithSignature[] memory input, bytes memory proof) - public + function verifyRRSet(RRSetWithSignature[] memory input) + external + view virtual - returns (bytes memory); + returns (bytes memory rrs, uint32 inception); - function submitRRSet(RRSetWithSignature memory input, bytes memory proof) + function verifyRRSet(RRSetWithSignature[] memory input, uint256 now) public - virtual - returns (bytes memory); - - function deleteRRSet( - uint16 deleteType, - bytes memory deleteName, - RRSetWithSignature memory nsec, - bytes memory proof - ) public virtual; - - function deleteRRSetNSEC3( - uint16 deleteType, - bytes memory deleteName, - RRSetWithSignature memory closestEncloser, - RRSetWithSignature memory nextClosest, - bytes memory dnskey - ) public virtual; - - function rrdata(uint16 dnstype, bytes calldata name) - external view virtual - returns ( - uint32, - uint32, - bytes20 - ); + returns (bytes memory rrs, uint32 inception); } diff --git a/contracts/dnssec-oracle/DNSSECImpl.sol b/contracts/dnssec-oracle/DNSSECImpl.sol index 2bfd9dab..81701d9f 100644 --- a/contracts/dnssec-oracle/DNSSECImpl.sol +++ b/contracts/dnssec-oracle/DNSSECImpl.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; pragma experimental ABIEncoderV2; @@ -7,13 +8,10 @@ import "./RRUtils.sol"; import "./DNSSEC.sol"; import "./algorithms/Algorithm.sol"; import "./digests/Digest.sol"; -import "./nsec3digests/NSEC3Digest.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; /* * @dev An oracle contract that verifies and stores DNSSEC-validated DNS records. - * - * TODO: Support for NSEC3 records */ contract DNSSECImpl is DNSSEC, Owned { using Buffer for Buffer.buffer; @@ -22,36 +20,24 @@ contract DNSSECImpl is DNSSEC, Owned { uint16 constant DNSCLASS_IN = 1; - uint16 constant DNSTYPE_NS = 2; - uint16 constant DNSTYPE_SOA = 6; - uint16 constant DNSTYPE_DNAME = 39; uint16 constant DNSTYPE_DS = 43; - uint16 constant DNSTYPE_RRSIG = 46; - uint16 constant DNSTYPE_NSEC = 47; uint16 constant DNSTYPE_DNSKEY = 48; - uint16 constant DNSTYPE_NSEC3 = 50; uint256 constant DNSKEY_FLAG_ZONEKEY = 0x100; - uint8 constant ALGORITHM_RSASHA256 = 8; - - uint8 constant DIGEST_ALGORITHM_SHA256 = 2; - - struct RRSet { - uint32 inception; - uint32 expiration; - bytes20 hash; - } - - // (name, type) => RRSet - mapping(bytes32 => mapping(uint16 => RRSet)) rrsets; + error InvalidLabelCount(bytes name, uint256 labelsExpected); + error SignatureNotValidYet(uint32 inception, uint32 now); + error SignatureExpired(uint32 expiration, uint32 now); + error InvalidClass(uint16 class); + error InvalidRRSet(); + error SignatureTypeMismatch(uint16 rrsetType, uint16 sigType); + error InvalidSignerName(bytes rrsetName, bytes signerName); + error InvalidProofType(uint16 proofType); + error ProofNameMismatch(bytes signerName, bytes proofName); + error NoMatchingProof(bytes signerName); mapping(uint8 => Algorithm) public algorithms; mapping(uint8 => Digest) public digests; - mapping(uint8 => NSEC3Digest) public nsec3Digests; - - event Test(uint256 t); - event Marker(); /** * @dev Constructor. @@ -61,12 +47,6 @@ contract DNSSECImpl is DNSSEC, Owned { // Insert the 'trust anchors' - the key hashes that start the chain // of trust for all other records. anchors = _anchors; - rrsets[keccak256(hex"00")][DNSTYPE_DS] = RRSet({ - inception: uint32(0), - expiration: uint32(3767581600), // May 22 2089 - the latest date we can encode as of writing this - hash: bytes20(keccak256(anchors)) - }); - emit RRSetUpdated(hex"00", anchors); } /** @@ -92,436 +72,72 @@ contract DNSSECImpl is DNSSEC, Owned { } /** - * @dev Sets the contract address for an NSEC3 digest algorithm. - * Callable only by the owner. - * @param id The digest ID - * @param digest The address of the digest contract. + * @dev Takes a chain of signed DNS records, verifies them, and returns the data from the last record set in the chain. + * Reverts if the records do not form an unbroken chain of trust to the DNSSEC anchor records. + * @param input A list of signed RRSets. + * @return rrs The RRData from the last RRSet in the chain. + * @return inception The inception time of the signed record set. */ - function setNSEC3Digest(uint8 id, NSEC3Digest digest) public owner_only { - nsec3Digests[id] = digest; - emit NSEC3DigestUpdated(id, address(digest)); - } - - /** - * @dev Submits multiple RRSets - * @param input A list of RRSets and signatures forming a chain of trust from an existing known-good record. - * @param proof The DNSKEY or DS to validate the first signature against. - * @return The last RRSET submitted. - */ - function submitRRSets(RRSetWithSignature[] memory input, bytes memory proof) - public + function verifyRRSet(RRSetWithSignature[] memory input) + external + view + virtual override - returns (bytes memory) + returns (bytes memory rrs, uint32 inception) { - for (uint256 i = 0; i < input.length; i++) { - proof = _submitRRSet(input[i], proof); - } - return proof; + return verifyRRSet(input, block.timestamp); } /** - * @dev Submits a signed set of RRs to the oracle. - * - * RRSETs are only accepted if they are signed with a key that is already - * trusted, or if they are self-signed, and the signing key is identified by - * a DS record that is already trusted. - * - * @param input The signed RR set. This is in the format described in section - * 5.3.2 of RFC4035: The RRDATA section from the RRSIG without the signature - * data, followed by a series of canonicalised RR records that the signature - * applies to. - * @param proof The DNSKEY or DS to validate the signature against. Must Already - * have been submitted and proved previously. + * @dev Takes a chain of signed DNS records, verifies them, and returns the data from the last record set in the chain. + * Reverts if the records do not form an unbroken chain of trust to the DNSSEC anchor records. + * @param input A list of signed RRSets. + * @param now The Unix timestamp to validate the records at. + * @return rrs The RRData from the last RRSet in the chain. + * @return inception The inception time of the signed record set. */ - function submitRRSet(RRSetWithSignature memory input, bytes memory proof) + function verifyRRSet(RRSetWithSignature[] memory input, uint256 now) public - override - returns (bytes memory) - { - return _submitRRSet(input, proof); - } - - /** - * @dev Deletes an RR from the oracle. - * - * @param deleteType The DNS record type to delete. - * @param deleteName which you want to delete - * @param nsec The signed NSEC RRset. This is in the format described in section - * 5.3.2 of RFC4035: The RRDATA section from the RRSIG without the signature - * data, followed by a series of canonicalised RR records that the signature - * applies to. - */ - function deleteRRSet( - uint16 deleteType, - bytes memory deleteName, - RRSetWithSignature memory nsec, - bytes memory proof - ) public override { - RRUtils.SignedSet memory rrset; - rrset = validateSignedSet(nsec, proof); - require(rrset.typeCovered == DNSTYPE_NSEC); - - // Don't let someone use an old proof to delete a new name - require( - RRUtils.serialNumberGte( - rrset.inception, - rrsets[keccak256(deleteName)][deleteType].inception - ) - ); - - for ( - RRUtils.RRIterator memory iter = rrset.rrs(); - !iter.done(); - iter.next() - ) { - // We're dealing with three names here: - // - deleteName is the name the user wants us to delete - // - nsecName is the owner name of the NSEC record - // - nextName is the next name specified in the NSEC record - // - // And three cases: - // - deleteName equals nsecName, in which case we can delete the - // record if it's not in the type bitmap. - // - nextName comes after nsecName, in which case we can delete - // the record if deleteName comes between nextName and nsecName. - // - nextName comes before nsecName, in which case nextName is the - // zone apex, and deleteName must come after nsecName. - checkNsecName(iter, rrset.name, deleteName, deleteType); - delete rrsets[keccak256(deleteName)][deleteType]; - return; - } - // We should never reach this point - revert(); - } - - function checkNsecName( - RRUtils.RRIterator memory iter, - bytes memory nsecName, - bytes memory deleteName, - uint16 deleteType - ) private pure { - uint256 rdataOffset = iter.rdataOffset; - uint256 nextNameLength = iter.data.nameLength(rdataOffset); - uint256 rDataLength = iter.nextOffset - iter.rdataOffset; - - // We assume that there is always typed bitmap after the next domain name - require(rDataLength > nextNameLength); - - int256 compareResult = deleteName.compareNames(nsecName); - if (compareResult == 0) { - // Name to delete is on the same label as the NSEC record - require( - !iter.data.checkTypeBitmap( - rdataOffset + nextNameLength, - deleteType - ) - ); - } else { - // First check if the NSEC next name comes after the NSEC name. - bytes memory nextName = iter.data.substring( - rdataOffset, - nextNameLength - ); - // deleteName must come after nsecName - require(compareResult > 0); - if (nsecName.compareNames(nextName) < 0) { - // deleteName must also come before nextName - require(deleteName.compareNames(nextName) < 0); - } - } - } - - /** - * @dev Deletes an RR from the oracle using an NSEC3 proof. - * Deleting a record using NSEC3 requires using up to two NSEC3 records. There are two cases: - * 1. The name exists, but the record type doesn't. Eg, example.com has A records but no TXT records. - * 2. The name does not exist, but a parent name does. - * In the first case, we submit one NSEC3 proof in `closestEncloser` that matches the target name - * but does not have the bit for `deleteType` set in its type bitmap. In the second case, we submit - * two proofs: closestEncloser and nextClosest, that together prove that the name does not exist. - * NSEC3 records are in the format described in section 5.3.2 of RFC4035: The RRDATA section - * from the RRSIG without the signature data, followed by a series of canonicalised RR records - * that the signature applies to. - * - * @param deleteType The DNS record type to delete. - * @param deleteName The name to delete. - * @param closestEncloser An NSEC3 proof matching the closest enclosing name - that is, - * the nearest ancestor of the target name that *does* exist. - * @param nextClosest An NSEC3 proof covering the next closest name. This proves that the immediate - * subdomain of the closestEncloser does not exist. - * @param dnskey An encoded DNSKEY record that has already been submitted to the oracle and can be used - * to verify the signatures closestEncloserSig and nextClosestSig - */ - function deleteRRSetNSEC3( - uint16 deleteType, - bytes memory deleteName, - RRSetWithSignature memory closestEncloser, - RRSetWithSignature memory nextClosest, - bytes memory dnskey - ) public override { - uint32 originalInception = rrsets[keccak256(deleteName)][deleteType] - .inception; - - RRUtils.SignedSet memory ce = validateSignedSet( - closestEncloser, - dnskey - ); - checkNSEC3Validity(ce, deleteName, originalInception); - - RRUtils.SignedSet memory nc; - if (nextClosest.rrset.length > 0) { - nc = validateSignedSet(nextClosest, dnskey); - checkNSEC3Validity(nc, deleteName, originalInception); - } - - RRUtils.NSEC3 memory ceNSEC3 = readNSEC3(ce); - // The flags field must be 0 or 1 (RFC5155 section 8.2). - require(ceNSEC3.flags & 0xfe == 0); - // Check that the closest encloser is from the correct zone (RFC5155 section 8.3) - // "The DNAME type bit must not be set and the NS type bit may only be set if the SOA type bit is set." - require( - !ceNSEC3.checkTypeBitmap(DNSTYPE_DNAME) && - (!ceNSEC3.checkTypeBitmap(DNSTYPE_NS) || - ceNSEC3.checkTypeBitmap(DNSTYPE_SOA)) - ); - - // Case 1: deleteName does exist, but no records of RRTYPE deleteType do. - if (isMatchingNSEC3Record(deleteType, deleteName, ce.name, ceNSEC3)) { - delete rrsets[keccak256(deleteName)][deleteType]; - // Case 2: deleteName does not exist. - } else if ( - isCoveringNSEC3Record( - deleteName, - ce.name, - ceNSEC3, - nc.name, - readNSEC3(nc) - ) - ) { - delete rrsets[keccak256(deleteName)][deleteType]; - } else { - revert(); - } - } - - function checkNSEC3Validity( - RRUtils.SignedSet memory nsec, - bytes memory deleteName, - uint32 originalInception - ) private pure { - // The records must have been signed after the record we're trying to delete - require(RRUtils.serialNumberGte(nsec.inception, originalInception)); - - // The record must be an NSEC3 - require(nsec.typeCovered == DNSTYPE_NSEC3); - - // nsecName is of the form .zone.xyz. is the NSEC3 hash of the entire name the NSEC3 record matches, while - // zone.xyz can be any ancestor of that name. We'll check that, so someone can't use a record on foo.com - // as proof of the nonexistence of bar.org. - require(checkNSEC3OwnerName(nsec.name, deleteName)); - } - - function isMatchingNSEC3Record( - uint16 deleteType, - bytes memory deleteName, - bytes memory closestEncloserName, - RRUtils.NSEC3 memory closestEncloser - ) private view returns (bool) { - // Check the record matches the hashed name, but the type bitmap does not include the type - if (checkNSEC3Name(closestEncloser, closestEncloserName, deleteName)) { - return !closestEncloser.checkTypeBitmap(deleteType); - } - - return false; - } - - function isCoveringNSEC3Record( - bytes memory deleteName, - bytes memory ceName, - RRUtils.NSEC3 memory ce, - bytes memory ncName, - RRUtils.NSEC3 memory nc - ) private view returns (bool) { - // The flags field must be 0 or 1 (RFC5155 section 8.2). - require(nc.flags & 0xfe == 0); - - bytes32 ceNameHash = decodeOwnerNameHash(ceName); - bytes32 ncNameHash = decodeOwnerNameHash(ncName); - - uint256 lastOffset = 0; - // Iterate over suffixes of the name to delete until one matches the closest encloser - for ( - uint256 offset = deleteName.readUint8(0) + 1; - offset < deleteName.length; - offset += deleteName.readUint8(offset) + 1 - ) { - if ( - hashName( - ce, - deleteName.substring(offset, deleteName.length - offset) - ) == ceNameHash - ) { - // Check that the next closest record encloses the name one label longer - bytes32 checkHash = hashName( - nc, - deleteName.substring( - lastOffset, - deleteName.length - lastOffset - ) - ); - if (ncNameHash < nc.nextHashedOwnerName) { - return - checkHash > ncNameHash && - checkHash < nc.nextHashedOwnerName; - } else { - return - checkHash > ncNameHash || - checkHash < nc.nextHashedOwnerName; - } - } - lastOffset = offset; - } - // If we reached the root without finding a match, return false. - return false; - } - - function readNSEC3(RRUtils.SignedSet memory ss) - private - pure - returns (RRUtils.NSEC3 memory) - { - RRUtils.RRIterator memory iter = ss.rrs(); - return - iter.data.readNSEC3( - iter.rdataOffset, - iter.nextOffset - iter.rdataOffset - ); - } - - function checkNSEC3Name( - RRUtils.NSEC3 memory nsec, - bytes memory ownerName, - bytes memory deleteName - ) private view returns (bool) { - // Compute the NSEC3 name hash of the name to delete. - bytes32 deleteNameHash = hashName(nsec, deleteName); - - // Decode the NSEC3 name hash from the first label of the NSEC3 owner name. - bytes32 nsecNameHash = decodeOwnerNameHash(ownerName); - - return deleteNameHash == nsecNameHash; - } - - function hashName(RRUtils.NSEC3 memory nsec, bytes memory name) - private - view - returns (bytes32) - { - return - nsec3Digests[nsec.hashAlgorithm].hash( - nsec.salt, - name, - nsec.iterations - ); - } - - function decodeOwnerNameHash(bytes memory name) - private - pure - returns (bytes32) - { - return name.base32HexDecodeWord(1, uint256(name.readUint8(0))); - } - - function checkNSEC3OwnerName(bytes memory nsecName, bytes memory deleteName) - private - pure - returns (bool) - { - uint256 nsecNameOffset = nsecName.readUint8(0) + 1; - uint256 deleteNameOffset = 0; - while (deleteNameOffset < deleteName.length) { - if (deleteName.equals(deleteNameOffset, nsecName, nsecNameOffset)) { - return true; - } - deleteNameOffset += deleteName.readUint8(deleteNameOffset) + 1; - } - return false; - } - - /** - * @dev Returns data about the RRs (if any) known to this oracle with the provided type and name. - * @param dnstype The DNS record type to query. - * @param name The name to query, in DNS label-sequence format. - * @return inception The unix timestamp (wrapped) at which the signature for this RRSET was created. - * @return expiration The unix timestamp (wrapped) at which the signature for this RRSET expires. - * @return hash The hash of the RRset. - */ - function rrdata(uint16 dnstype, bytes calldata name) - external view + virtual override - returns ( - uint32, - uint32, - bytes20 - ) + returns (bytes memory rrs, uint32 inception) { - RRSet storage result = rrsets[keccak256(name)][dnstype]; - return (result.inception, result.expiration, result.hash); - } - - function _submitRRSet(RRSetWithSignature memory input, bytes memory proof) - internal - returns (bytes memory) - { - RRUtils.SignedSet memory rrset; - rrset = validateSignedSet(input, proof); - - RRSet storage storedSet = rrsets[keccak256(rrset.name)][ - rrset.typeCovered - ]; - if (storedSet.hash != bytes20(0)) { - // To replace an existing rrset, the signature must be at least as new - require( - RRUtils.serialNumberGte(rrset.inception, storedSet.inception) + bytes memory proof = anchors; + for (uint256 i = 0; i < input.length; i++) { + RRUtils.SignedSet memory rrset = validateSignedSet( + input[i], + proof, + now ); + proof = rrset.data; + inception = rrset.inception; } - rrsets[keccak256(rrset.name)][rrset.typeCovered] = RRSet({ - inception: rrset.inception, - expiration: rrset.expiration, - hash: bytes20(keccak256(rrset.data)) - }); - - emit RRSetUpdated(rrset.name, rrset.data); - - return rrset.data; + return (proof, inception); } /** - * @dev Submits a signed set of RRs to the oracle. - * - * RRSETs are only accepted if they are signed with a key that is already - * trusted, or if they are self-signed, and the signing key is identified by - * a DS record that is already trusted. + * @dev Validates an RRSet against the already trusted RR provided in `proof`. * * @param input The signed RR set. This is in the format described in section * 5.3.2 of RFC4035: The RRDATA section from the RRSIG without the signature * data, followed by a series of canonicalised RR records that the signature * applies to. - * @param proof The DNSKEY or DS to validate the signature against. Must Already - * have been submitted and proved previously. + * @param proof The DNSKEY or DS to validate the signature against. + * @param now The current timestamp. */ function validateSignedSet( RRSetWithSignature memory input, - bytes memory proof + bytes memory proof, + uint256 now ) internal view returns (RRUtils.SignedSet memory rrset) { rrset = input.rrset.readSignedSet(); - require(validProof(rrset.signerName, proof)); // Do some basic checks on the RRs and extract the name bytes memory name = validateRRs(rrset, rrset.typeCovered); - require(name.labelCount(0) == rrset.labels); + if (name.labelCount(0) != rrset.labels) { + revert InvalidLabelCount(name, rrset.labels); + } rrset.name = name; // All comparisons involving the Signature Expiration and @@ -530,15 +146,15 @@ contract DNSSECImpl is DNSSEC, Owned { // o The validator's notion of the current time MUST be less than or // equal to the time listed in the RRSIG RR's Expiration field. - require( - RRUtils.serialNumberGte(rrset.expiration, uint32(block.timestamp)) - ); + if (!RRUtils.serialNumberGte(rrset.expiration, uint32(now))) { + revert SignatureExpired(rrset.expiration, uint32(now)); + } // o The validator's notion of the current time MUST be greater than or // equal to the time listed in the RRSIG RR's Inception field. - require( - RRUtils.serialNumberGte(uint32(block.timestamp), rrset.inception) - ); + if (!RRUtils.serialNumberGte(uint32(now), rrset.inception)) { + revert SignatureNotValidYet(rrset.inception, uint32(now)); + } // Validate the signature verifySignature(name, rrset, input, proof); @@ -546,16 +162,6 @@ contract DNSSECImpl is DNSSEC, Owned { return rrset; } - function validProof(bytes memory name, bytes memory proof) - internal - view - returns (bool) - { - uint16 dnstype = proof.readUint16(proof.nameLength(0)); - return - rrsets[keccak256(name)][dnstype].hash == bytes20(keccak256(proof)); - } - /** * @dev Validates a set of RRs. * @param rrset The RR set. @@ -573,19 +179,27 @@ contract DNSSECImpl is DNSSEC, Owned { iter.next() ) { // We only support class IN (Internet) - require(iter.class == DNSCLASS_IN); + if (iter.class != DNSCLASS_IN) { + revert InvalidClass(iter.class); + } if (name.length == 0) { name = iter.name(); } else { // Name must be the same on all RRs. We do things this way to avoid copying the name // repeatedly. - require(name.length == iter.data.nameLength(iter.offset)); - require(name.equals(0, iter.data, iter.offset, name.length)); + if ( + name.length != iter.data.nameLength(iter.offset) || + !name.equals(0, iter.data, iter.offset, name.length) + ) { + revert InvalidRRSet(); + } } // o The RRSIG RR's Type Covered field MUST equal the RRset's type. - require(iter.dnstype == typecovered); + if (iter.dnstype != typecovered) { + revert SignatureTypeMismatch(iter.dnstype, typecovered); + } } } @@ -606,23 +220,25 @@ contract DNSSECImpl is DNSSEC, Owned { ) internal view { // o The RRSIG RR's Signer's Name field MUST be the name of the zone // that contains the RRset. - require(rrset.signerName.length <= name.length); - require( - rrset.signerName.equals( + if ( + rrset.signerName.length > name.length || + !rrset.signerName.equals( 0, name, name.length - rrset.signerName.length ) - ); + ) { + revert InvalidSignerName(name, rrset.signerName); + } RRUtils.RRIterator memory proofRR = proof.iterateRRs(0); // Check the proof if (proofRR.dnstype == DNSTYPE_DS) { - require(verifyWithDS(rrset, data, proofRR)); + verifyWithDS(rrset, data, proofRR); } else if (proofRR.dnstype == DNSTYPE_DNSKEY) { - require(verifyWithKnownKey(rrset, data, proofRR)); + verifyWithKnownKey(rrset, data, proofRR); } else { - revert("No valid proof found"); + revert InvalidProofType(proofRR.dnstype); } } @@ -631,27 +247,29 @@ contract DNSSECImpl is DNSSEC, Owned { * @param rrset The signed set to verify. * @param data The original data the signed set was read from. * @param proof The serialized DS or DNSKEY record to use as proof. - * @return True if the RRSET could be verified, false otherwise. */ function verifyWithKnownKey( RRUtils.SignedSet memory rrset, RRSetWithSignature memory data, RRUtils.RRIterator memory proof - ) internal view returns (bool) { + ) internal view { // Check the DNSKEY's owner name matches the signer name on the RRSIG - require(proof.name().equals(rrset.signerName)); for (; !proof.done(); proof.next()) { - require(proof.name().equals(rrset.signerName)); + bytes memory proofName = proof.name(); + if (!proofName.equals(rrset.signerName)) { + revert ProofNameMismatch(rrset.signerName, proofName); + } + bytes memory keyrdata = proof.rdata(); RRUtils.DNSKEY memory dnskey = keyrdata.readDNSKEY( 0, keyrdata.length ); if (verifySignatureWithKey(dnskey, keyrdata, rrset, data)) { - return true; + return; } } - return false; + revert NoMatchingProof(rrset.signerName); } /** @@ -702,19 +320,21 @@ contract DNSSECImpl is DNSSEC, Owned { * @param rrset The signed set to verify. * @param data The original data the signed set was read from. * @param proof The serialized DS or DNSKEY record to use as proof. - * @return True if the RRSET could be verified, false otherwise. */ function verifyWithDS( RRUtils.SignedSet memory rrset, RRSetWithSignature memory data, RRUtils.RRIterator memory proof - ) internal view returns (bool) { + ) internal view { for ( RRUtils.RRIterator memory iter = rrset.rrs(); !iter.done(); iter.next() ) { - require(iter.dnstype == DNSTYPE_DNSKEY); + if (iter.dnstype != DNSTYPE_DNSKEY) { + revert InvalidProofType(iter.dnstype); + } + bytes memory keyrdata = iter.rdata(); RRUtils.DNSKEY memory dnskey = keyrdata.readDNSKEY( 0, @@ -722,10 +342,14 @@ contract DNSSECImpl is DNSSEC, Owned { ); if (verifySignatureWithKey(dnskey, keyrdata, rrset, data)) { // It's self-signed - look for a DS record to verify it. - return verifyKeyWithDS(iter.name(), proof, dnskey, keyrdata); + if ( + verifyKeyWithDS(rrset.signerName, proof, dnskey, keyrdata) + ) { + return; + } } } - return false; + revert NoMatchingProof(rrset.signerName); } /** @@ -744,6 +368,11 @@ contract DNSSECImpl is DNSSEC, Owned { ) internal view returns (bool) { uint16 keytag = keyrdata.computeKeytag(); for (; !dsrrs.done(); dsrrs.next()) { + bytes memory proofName = dsrrs.name(); + if (!proofName.equals(keyname)) { + revert ProofNameMismatch(keyname, proofName); + } + RRUtils.DS memory ds = dsrrs.data.readDS( dsrrs.rdataOffset, dsrrs.nextOffset - dsrrs.rdataOffset diff --git a/contracts/dnssec-oracle/RRUtils.sol b/contracts/dnssec-oracle/RRUtils.sol index 8081e574..ad5560c4 100644 --- a/contracts/dnssec-oracle/RRUtils.sol +++ b/contracts/dnssec-oracle/RRUtils.sol @@ -266,91 +266,6 @@ library RRUtils { self.digest = data.substring(offset + DS_DIGEST, length - DS_DIGEST); } - struct NSEC3 { - uint8 hashAlgorithm; - uint8 flags; - uint16 iterations; - bytes salt; - bytes32 nextHashedOwnerName; - bytes typeBitmap; - } - - uint256 constant NSEC3_HASH_ALGORITHM = 0; - uint256 constant NSEC3_FLAGS = 1; - uint256 constant NSEC3_ITERATIONS = 2; - uint256 constant NSEC3_SALT_LENGTH = 4; - uint256 constant NSEC3_SALT = 5; - - function readNSEC3( - bytes memory data, - uint256 offset, - uint256 length - ) internal pure returns (NSEC3 memory self) { - uint256 end = offset + length; - self.hashAlgorithm = data.readUint8(offset + NSEC3_HASH_ALGORITHM); - self.flags = data.readUint8(offset + NSEC3_FLAGS); - self.iterations = data.readUint16(offset + NSEC3_ITERATIONS); - uint8 saltLength = data.readUint8(offset + NSEC3_SALT_LENGTH); - offset = offset + NSEC3_SALT; - self.salt = data.substring(offset, saltLength); - offset += saltLength; - uint8 nextLength = data.readUint8(offset); - require(nextLength <= 32); - offset += 1; - self.nextHashedOwnerName = data.readBytesN(offset, nextLength); - offset += nextLength; - self.typeBitmap = data.substring(offset, end - offset); - } - - function checkTypeBitmap(NSEC3 memory self, uint16 rrtype) - internal - pure - returns (bool) - { - return checkTypeBitmap(self.typeBitmap, 0, rrtype); - } - - /** - * @dev Checks if a given RR type exists in a type bitmap. - * @param bitmap The byte string to read the type bitmap from. - * @param offset The offset to start reading at. - * @param rrtype The RR type to check for. - * @return True if the type is found in the bitmap, false otherwise. - */ - function checkTypeBitmap( - bytes memory bitmap, - uint256 offset, - uint16 rrtype - ) internal pure returns (bool) { - uint8 typeWindow = uint8(rrtype >> 8); - uint8 windowByte = uint8((rrtype & 0xff) / 8); - uint8 windowBitmask = uint8( - uint8(1) << (uint8(7) - uint8(rrtype & 0x7)) - ); - for (uint256 off = offset; off < bitmap.length; ) { - uint8 window = bitmap.readUint8(off); - uint8 len = bitmap.readUint8(off + 1); - if (typeWindow < window) { - // We've gone past our window; it's not here. - return false; - } else if (typeWindow == window) { - // Check this type bitmap - if (len <= windowByte) { - // Our type is past the end of the bitmap - return false; - } - return - (bitmap.readUint8(off + windowByte + 2) & windowBitmask) != - 0; - } else { - // Skip this type bitmap - off += len + 2; - } - } - - return false; - } - function compareNames(bytes memory self, bytes memory other) internal pure diff --git a/contracts/dnssec-oracle/nsec3digests/NSEC3Digest.sol b/contracts/dnssec-oracle/nsec3digests/NSEC3Digest.sol deleted file mode 100644 index 0a0b4395..00000000 --- a/contracts/dnssec-oracle/nsec3digests/NSEC3Digest.sol +++ /dev/null @@ -1,19 +0,0 @@ -pragma solidity ^0.8.4; - -/** - * @dev Interface for contracts that implement NSEC3 digest algorithms. - */ -interface NSEC3Digest { - /** - * @dev Performs an NSEC3 iterated hash. - * @param salt The salt value to use on each iteration. - * @param data The data to hash. - * @param iterations The number of iterations to perform. - * @return The result of the iterated hash operation. - */ - function hash( - bytes calldata salt, - bytes calldata data, - uint256 iterations - ) external pure virtual returns (bytes32); -} diff --git a/contracts/dnssec-oracle/nsec3digests/SHA1NSEC3Digest.sol b/contracts/dnssec-oracle/nsec3digests/SHA1NSEC3Digest.sol deleted file mode 100644 index 39bf7350..00000000 --- a/contracts/dnssec-oracle/nsec3digests/SHA1NSEC3Digest.sol +++ /dev/null @@ -1,37 +0,0 @@ -pragma solidity ^0.8.4; - -import "./NSEC3Digest.sol"; -import "../SHA1.sol"; -import "@ensdomains/buffer/contracts/Buffer.sol"; - -/** - * @dev Implements the DNSSEC iterated SHA1 digest used for NSEC3 records. - */ -contract SHA1NSEC3Digest is NSEC3Digest { - using Buffer for Buffer.buffer; - - function hash( - bytes calldata salt, - bytes calldata data, - uint256 iterations - ) external pure override returns (bytes32) { - Buffer.buffer memory buf; - buf.init(salt.length + data.length + 16); - - buf.append(data); - buf.append(salt); - bytes20 h = SHA1.sha1(buf.buf); - if (iterations > 0) { - buf.truncate(); - buf.appendBytes20(bytes20(0)); - buf.append(salt); - - for (uint256 i = 0; i < iterations; i++) { - buf.writeBytes20(0, h); - h = SHA1.sha1(buf.buf); - } - } - - return bytes32(h); - } -} diff --git a/contracts/ethregistrar/mocks/DummyDNSSEC.sol b/contracts/ethregistrar/mocks/DummyDNSSEC.sol deleted file mode 100644 index 23836c95..00000000 --- a/contracts/ethregistrar/mocks/DummyDNSSEC.sol +++ /dev/null @@ -1,54 +0,0 @@ -pragma solidity >=0.8.4; - -import "../../registry/ENSRegistry.sol"; -import "../../dnssec-oracle/DNSSEC.sol"; - -contract DummyDnsRegistrarDNSSEC { - struct Data { - uint32 inception; - uint64 inserted; - bytes20 hash; - } - - mapping(bytes32 => Data) private datas; - - function setData( - uint16 _expectedType, - bytes memory _expectedName, - uint32 _inception, - uint64 _inserted, - bytes memory _proof - ) public { - Data storage rr = datas[ - keccak256(abi.encodePacked(_expectedType, _expectedName)) - ]; - rr.inception = _inception; - rr.inserted = _inserted; - - if (_proof.length != 0) { - rr.hash = bytes20(keccak256(_proof)); - } else { - rr.hash = bytes20(0); - } - } - - function rrdata(uint16 dnstype, bytes memory name) - public - view - returns ( - uint32, - uint64, - bytes20 - ) - { - Data storage rr = datas[keccak256(abi.encodePacked(dnstype, name))]; - return (rr.inception, rr.inserted, rr.hash); - } - - function submitRRSets( - DNSSEC.RRSetWithSignature[] memory input, - bytes calldata - ) public virtual returns (bytes memory) { - return input[input.length - 1].rrset; - } -} diff --git a/deploy/dnssec-oracle/10_deploy_oracle.ts b/deploy/dnssec-oracle/10_deploy_oracle.ts index f7d86935..64fb5041 100644 --- a/deploy/dnssec-oracle/10_deploy_oracle.ts +++ b/deploy/dnssec-oracle/10_deploy_oracle.ts @@ -76,9 +76,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 1: 'SHA1Digest', 2: 'SHA256Digest', } - const nsec_digests: Record = { - 1: 'SHA1NSEC3Digest', - } if (network.tags.test) { anchors.push(dummyAnchor) @@ -109,13 +106,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { } } - for (const [id, digest] of Object.entries(nsec_digests)) { - const address = (await deployments.get(digest)).address - if (address != (await dnssec.nsec3Digests(id))) { - transactions.push(await dnssec.setNSEC3Digest(id, address)) - } - } - console.log( `Waiting on ${transactions.length} transactions setting DNSSEC parameters`, ) diff --git a/hardhat.config.ts b/hardhat.config.ts index 261f4216..c03af3c3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -128,8 +128,6 @@ if (process.env.DEPLOYER_KEY) { const config: HardhatUserConfig = { networks: { hardhat: { - // Required for real DNS record tests - initialDate: '2019-03-15T14:06:45.000+13:00', saveDeployments: false, tags: ['test', 'legacy', 'use_root'], }, diff --git a/index.js b/index.js index 73a6491c..f84f5408 100644 --- a/index.js +++ b/index.js @@ -25,7 +25,6 @@ const RSASHA256Algorithm = require('./build/contracts/RSASHA256Algorithm') const RSASHA1Algorithm = require('./build/contracts/RSASHA1Algorithm') const SHA256Digest = require('./build/contracts/SHA256Digest') const SHA1Digest = require('./build/contracts/SHA1Digest') -const SHA1NSEC3Digest = require('./build/contracts/SHA1NSEC3Digest') module.exports = { BaseRegistrar, @@ -54,5 +53,4 @@ module.exports = { RSASHA1Algorithm, SHA256Digest, SHA1Digest, - SHA1NSEC3Digest, } diff --git a/package.json b/package.json index bc10667a..7f0454c3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "hardhat-deploy": "^0.11.10", "hardhat-gas-reporter": "^1.0.4", "prettier": "^2.6.2", + "prettier-plugin-solidity": "^1.0.0-beta.24", "rfc4648": "^1.5.0", "ts-node": "^10.8.1", "typescript": "^4.7.3" diff --git a/test/dnsregistrar/TestDNSRegistrar.js b/test/dnsregistrar/TestDNSRegistrar.js index 158d59d8..1b1db36a 100644 --- a/test/dnsregistrar/TestDNSRegistrar.js +++ b/test/dnsregistrar/TestDNSRegistrar.js @@ -1,13 +1,14 @@ const ENSRegistry = artifacts.require('./ENSRegistry.sol') -const DummyDNSSEC = artifacts.require('./DummyDnsRegistrarDNSSEC.sol') const Root = artifacts.require('/Root.sol') const SimplePublixSuffixList = artifacts.require('./SimplePublicSuffixList.sol') const DNSRegistrarContract = artifacts.require('./DNSRegistrar.sol') const PublicResolver = artifacts.require('./PublicResolver.sol') +const DNSSECImpl = artifacts.require('./DNSSECImpl') const namehash = require('eth-ens-namehash') const utils = require('./Helpers/Utils') const { exceptions } = require('@ensdomains/test-utils') const { assert } = require('chai') +const { rootKeys, hexEncodeSignedSet } = require('../utils/dnsutils.js') const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -18,14 +19,47 @@ contract('DNSRegistrar', function(accounts) { var dnssec = null var suffixes = null var now = Math.round(new Date().getTime() / 1000) - + const validityPeriod = 2419200 + const expiration = Date.now() / 1000 - 15 * 60 + validityPeriod + const inception = Date.now() / 1000 - 15 * 60 + const testRrset = (name, account) => ({ + name, + sig: { + name: 'test', + type: 'RRSIG', + ttl: 0, + class: 'IN', + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: name.split('.').length + 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, + }, + rrs: [ + { + name: `_ens.${name}`, + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from(`a=${account}`, 'ascii'), + }, + ], + }); + beforeEach(async function() { ens = await ENSRegistry.new() root = await Root.new(ens.address) await ens.setOwner('0x0', root.address) - dnssec = await DummyDNSSEC.new() + dnssec = await DNSSECImpl.deployed() suffixes = await SimplePublixSuffixList.new() await suffixes.addPublicSuffixes([ @@ -45,147 +79,100 @@ contract('DNSRegistrar', function(accounts) { assert.equal(await registrar.oracle(), dnssec.address) assert.equal(await registrar.ens(), ens.address) - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[1]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) - - await registrar.claim(utils.hexEncodeName('foo.test'), proof) - - assert.equal(await ens.owner(namehash.hash('foo.test')), accounts[1]) - }) - - it('allows the owner to prove-and-claim', async () => { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ]; await registrar.proveAndClaim( utils.hexEncodeName('foo.test'), - [{ rrset: proof, sig: '0x' }], - '0x' + proof, + {from: accounts[1]} ) assert.equal(await ens.owner(namehash.hash('foo.test')), accounts[0]) }) it('allows claims on names that are not TLDs', async function() { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.co.nz', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.co.nz'), - now, - now, + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.co.nz', accounts[0])) + ]; + + await registrar.proveAndClaim( + utils.hexEncodeName('foo.co.nz'), proof ) - await registrar.claim(utils.hexEncodeName('foo.co.nz'), proof) - assert.equal(await ens.owner(namehash.hash('foo.co.nz')), accounts[0]) }) - it('allows anyone to zero out an obsolete name', async function() { - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - '0x' + it('allows anyone to update a DNSSEC referenced name', async function() { + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ] + + await registrar.proveAndClaim( + utils.hexEncodeName('foo.test'), + proof ) - await registrar.claim(utils.hexEncodeName('foo.test'), '0x') + proof[1] = hexEncodeSignedSet(testRrset('foo.test', accounts[1])) - assert.equal(await ens.owner(namehash.hash('foo.test')), 0) + await registrar.proveAndClaim( + utils.hexEncodeName('foo.test'), + proof + ) + + assert.equal(await ens.owner(namehash.hash('foo.test')), accounts[1]) }) - it('allows anyone to update a DNSSEC referenced name', async function() { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[1]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, + it('rejects proofs with earlier inceptions', async function() { + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ] + + await registrar.proveAndClaim( + utils.hexEncodeName('foo.test'), proof ) - await registrar.claim(utils.hexEncodeName('foo.test'), proof) - assert.equal(await ens.owner(namehash.hash('foo.test')), accounts[1]) + const newRrset = testRrset('foo.test', accounts[1]) + console.log(newRrset) + newRrset.sig.data.inception -= 3600 + proof[1] = hexEncodeSignedSet(newRrset) + + await exceptions.expectFailure(registrar.proveAndClaim( + utils.hexEncodeName('foo.test'), + proof + )) }) it('does not allow updates with stale records', async function() { - var proof = utils.hexEncodeTXT({ - name: '_ens.bar.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData(16, utils.hexEncodeName('_ens.foo.test'), 0, 0, proof) + const rrSet = testRrset('foo.test', accounts[0]); + rrSet.sig.data.inception = Date.now() / 1000 - 120 + rrSet.sig.data.expiration = Date.now() / 1000 - 60 + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(rrSet) + ]; await exceptions.expectFailure( - registrar.claim(utils.hexEncodeName('bar.test'), proof) + registrar.proveAndClaim(utils.hexEncodeName('foo.test'), proof) ) }) it('allows the owner to claim and set a resolver', async () => { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ]; await registrar.proveAndClaimWithResolver( utils.hexEncodeName('foo.test'), - [{ rrset: proof, sig: '0x' }], - '0x', + proof, accounts[1], ZERO_ADDRESS ) @@ -195,27 +182,15 @@ contract('DNSRegistrar', function(accounts) { }) it('does not allow anyone else to claim and set a resolver', async () => { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[1]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[1])) + ]; await exceptions.expectFailure( registrar.proveAndClaimWithResolver( utils.hexEncodeName('foo.test'), - [{ rrset: proof, sig: '0x' }], - '0x', + proof, accounts[1], ZERO_ADDRESS ) @@ -230,26 +205,14 @@ contract('DNSRegistrar', function(accounts) { ZERO_ADDRESS ) - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ]; await registrar.proveAndClaimWithResolver( utils.hexEncodeName('foo.test'), - [{ rrset: proof, sig: '0x' }], - '0x', + proof, resolver.address, accounts[0] ) @@ -258,27 +221,15 @@ contract('DNSRegistrar', function(accounts) { }) it('forbids setting an address if the resolver is not also set', async () => { - var proof = utils.hexEncodeTXT({ - name: '_ens.foo.test', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: ['a=' + accounts[0]], - }) - - await dnssec.setData( - 16, - utils.hexEncodeName('_ens.foo.test'), - now, - now, - proof - ) + const proof = [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet(testRrset('foo.test', accounts[0])) + ]; await exceptions.expectFailure( registrar.proveAndClaimWithResolver( utils.hexEncodeName('foo.test'), - [{ rrset: proof, sig: '0x' }], - '0x', + proof, ZERO_ADDRESS, accounts[0] ) diff --git a/test/dnssec-oracle/TestDNSSEC.js b/test/dnssec-oracle/TestDNSSEC.js index 7ffa543a..0ed0868c 100644 --- a/test/dnssec-oracle/TestDNSSEC.js +++ b/test/dnssec-oracle/TestDNSSEC.js @@ -3,13 +3,17 @@ const anchors = require('../test-utils/anchors.js') const packet = require('dns-packet') const types = require('dns-packet/types') const { expectRevert } = require('@openzeppelin/test-helpers') +const { rootKeys, hexEncodeSignedSet, hexEncodeName } = require('../utils/dnsutils.js') var dnssec = artifacts.require('./DNSSECImpl') const SignedSet = require('@ensdomains/dnsprovejs').SignedSet const util = require('util') +const { expect } = require('chai') web3.currentProvider.send = util.promisify(web3.currentProvider.send) +const test_rrset_timestamp = 1552658805; + // When the real test start failing due to ttl expiration, you can generate the new test dataset at https://dnssec.ens.domains/?domain=ethlab.xyz&mode=advanced const test_rrsets = [ // . 55430 IN RRSIG DNSKEY 8 0 172800 20190402000000 20190312000000 20326 . A76nZ8WVsD+pLAKJh9ujKxxRDWfJf8SxayOkq3Gq9TX4BStpQM1e/KuX8am4FrVRCGQvLlhiYFNqm+PtevGGJAO0lTFLSiIuavknlkSiI3HMkrMDqSV+YlIQPk1C720khNpWy70WjjNvkq4sBU1GTkVPeFkM3gQI53pCHW+VobCPXZz70J+PnSOq7SmjrwXgU8E9iSXkI3yfhGIup2c54Sf9w0Bw10opvxXMT+1ALgWY1TnV1/gRixIUZp1K86iR8VeX9K/4UTqEa5bYux+aeIcQ2/4Qqyo3Ocb2RrbUvDNzU2lB4b1r/oHqsd6C0SiGmdo0A8R44djKMHVaD/JmLg== @@ -68,53 +72,19 @@ const test_rrsets = [ ], ] -function hexEncodeSignedSet(keys) { - const ss = new SignedSet(keys.rrs, keys.sig) - return [ss.toWire(), ss.signature.data.signature] -} - -function hexEncodeName(name) { - return '0x' + packet.name.encode(name).toString('hex') +async function verifySubmission(instance, rrsets) { + var response = await instance.verifyRRSet(rrsets); } -async function verifySubmission(instance, data, sig, proof) { - if (proof === undefined) { - proof = await instance.anchors() - } - - var tx = await instance.submitRRSet([data, sig], proof) - - assert.equal(tx.receipt.status, true) - assert.equal(tx.logs.length, 1) - return tx -} - -async function verifyFailedSubmission(instance, data, sig, proof) { - if (proof === undefined) { - proof = await instance.anchors() - } - - try { - var tx = expectRevert.unspecified( - await instance.submitRRSet([data, sig], proof) - ) - } catch (error) { - // @TODO use: https://github.com/ensdomains/root/blob/master/test/helpers/Utils.js#L8 - // Assert ganache revert exception - - assert.equal(error.message, 'Transaction reverted without a reason string') - } - - // Assert geth failed transaction - if (tx !== undefined) { - assert.equal(tx.receipt.status, false) - } +async function verifyFailedSubmission(instance, rrsets) { + await expectRevert.unspecified( + instance.verifyRRSet(rrsets) + ); } // Test against real record contract('DNSSEC', (accounts) => { it('should accept real DNSSEC records', async function() { - await network.provider.send('evm_mine') var instance = await dnssec.deployed() var proof = await instance.anchors() const totalLen = test_rrsets @@ -128,28 +98,14 @@ contract('DNSSEC', (accounts) => { const sigBuf = Buffer.from(sig.slice(2), 'hex') sets.push([rrsetBuf, sigBuf]) } - var tx = await instance.submitRRSets(sets, proof) - assert.equal(tx.receipt.status, true) + const { rrs, inception } = await instance.verifyRRSet(sets, test_rrset_timestamp); + var [_, data, sig] = test_rrsets[test_rrsets.length - 1]; + var expected = SignedSet.fromWire(Buffer.from(data.slice(2), 'hex'), Buffer.from(sig.slice(2), 'hex')); + expect(rrs.slice(2)).to.equal(expected.toWire(false).toString('hex')); }) }) contract('DNSSEC', function(accounts) { - before(async () => { - // Advance to the current time so the DNSSEC root keys work. - await network.provider.send('evm_setNextBlockTimestamp', [ - Date.now() / 1000, - ]) - await network.provider.send('evm_mine') - - const instance = await dnssec.deployed() - const keys = rootKeys() - const [signedData] = hexEncodeSignedSet(keys) - await instance.submitRRSet( - [signedData, Buffer.alloc(0)], - anchors.encode(anchors.realEntries) - ) - }) - let result beforeEach(async () => { ;({ result } = await web3.currentProvider.send({ @@ -197,79 +153,22 @@ contract('DNSSEC', function(accounts) { ) }) - it('should only allow the owner to set NSEC3 digests', async function() { - var instance = await dnssec.deployed() - await expectRevert.unspecified( - instance.setNSEC3Digest(1, accounts[1], { from: accounts[1] }) - ) - }) - const validityPeriod = 2419200 const expiration = Date.now() / 1000 - 15 * 60 + validityPeriod const inception = Date.now() / 1000 - 15 * 60 - function rootKeys() { - var name = '.' - var sig = { - name: '.', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'DNSKEY', - algorithm: 253, - labels: 0, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - } - - var rrs = [ - { - name: '.', - type: 'DNSKEY', - class: 'IN', - ttl: 3600, - data: { flags: 0, algorithm: 253, key: Buffer.from('0000', 'HEX') }, - }, - { - name: '.', - type: 'DNSKEY', - class: 'IN', - ttl: 3600, - data: { flags: 0, algorithm: 253, key: Buffer.from('1112', 'HEX') }, - }, - { - name: '.', - type: 'DNSKEY', - class: 'IN', - ttl: 3600, - data: { - flags: 0x0101, - algorithm: 253, - key: Buffer.from('0000', 'HEX'), - }, - }, - ] - return { name, sig, rrs } - } it('should reject signatures with non-matching algorithms', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() + var keys = rootKeys(expiration, inception) keys.rrs.forEach((r) => { r.data.algorithm = 255 }) - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) + await expectRevert(instance.verifyRRSet([hexEncodeSignedSet(keys)]), 'NoMatchingProof'); }) it('should reject signatures with non-matching keytags', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() + var keys = rootKeys(expiration, inception) keys.rrs = [ { @@ -286,12 +185,12 @@ contract('DNSSEC', function(accounts) { }, ] - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) + await expectRevert(instance.verifyRRSet([hexEncodeSignedSet(keys)]), 'NoMatchingProof'); }) it('should accept odd-length public keys', async () => { const instance = await dnssec.deployed() - const keys = rootKeys() + const keys = rootKeys(expiration, inception) keys.rrs = [ { name: '.', @@ -303,13 +202,12 @@ contract('DNSSEC', function(accounts) { }, }, ] - const [signedData] = hexEncodeSignedSet(keys) - await verifySubmission(instance, signedData, Buffer.alloc(0)) + await verifySubmission(instance, [hexEncodeSignedSet(keys)]) }) it('should reject signatures by keys without the ZK bit set', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() + var keys = rootKeys(expiration, inception) keys.rrs = [ { name: '.', @@ -325,217 +223,170 @@ contract('DNSSEC', function(accounts) { }, ] - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) + await expectRevert(instance.verifyRRSet([hexEncodeSignedSet(keys)]), 'NoMatchingProof'); }) - const { rrs } = rootKeys() - const rootKeyProof = anchors.encode(rrs) it('should accept a root DNSKEY', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() - await verifySubmission(instance, ...hexEncodeSignedSet(keys)) - }) - - it('should check if root DNSKEY exist', async function() { - var instance = await dnssec.deployed() - var result = await instance.rrdata.call( - types.toType('DNSKEY'), - hexEncodeName('nonexisting.') - ) - var rrs = result['2'] - assert.equal(rrs, '0x0000000000000000000000000000000000000000') - result = await instance.rrdata.call( - types.toType('DNSKEY'), - hexEncodeName('.') - ) - rrs = result['2'] - assert.notEqual(rrs, '0x0000000000000000000000000000000000000000') + var keys = rootKeys(expiration, inception) + await verifySubmission(instance, [hexEncodeSignedSet(keys)]) }) it('should accept a signed RRSET', async function() { var instance = await dnssec.deployed() await verifySubmission( instance, - hexEncodeSignedSet({ - name: 'test', - sig: { + [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'test', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'test', - type: 'TXT', + type: 'RRSIG', + ttl: 0, class: 'IN', - ttl: 3600, - data: Buffer.from('test', 'ascii'), + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, }, - ], - })[0], - '0x', - rootKeyProof - ) - }) - - it('should reject signatures with non-matching classes', async function() { - var instance = await dnssec.deployed() - await verifyFailedSubmission( - instance, - ...hexEncodeSignedSet({ - name: 'net', - sig: { + rrs: [ + { + name: 'test', + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from('test', 'ascii'), + }, + ], + }) + ] + ) + }) + + it('should reject signatures with non-IN classes', async function() { + var instance = await dnssec.deployed() + await expectRevert( + instance.verifyRRSet([ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'net', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'net', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: Buffer.from('foo', 'ascii'), + type: 'RRSIG', + ttl: 0, + class: 'CH', + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, }, - ], - }) - ) - }) - - it('should reject signatures with non-matching names', async function() { - var instance = await dnssec.deployed() - await verifyFailedSubmission( - instance, - ...hexEncodeSignedSet({ - name: 'foo.net', - sig: { - name: 'foo.net', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { - name: 'foo.net', - type: 'TXT', - class: 'IN', - ttl: 3600, - data: Buffer.from('foo', 'ascii'), - }, - ], - }) + rrs: [ + { + name: 'net', + type: 'TXT', + class: 'CH', + ttl: 3600, + data: Buffer.from('foo', 'ascii'), + }, + ], + }) + ]), + 'InvalidClass' ) }) it('should reject signatures with the wrong type covered', async function() { var instance = await dnssec.deployed() - await verifyFailedSubmission( - instance, - ...hexEncodeSignedSet({ - name: 'net', - sig: { + await expectRevert( + instance.verifyRRSet([ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'net', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'DS', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'net', - type: 'TXT', + type: 'RRSIG', + ttl: 0, class: 'IN', - ttl: 3600, - data: Buffer.from('foo', 'ascii'), + flush: false, + data: { + typeCovered: 'DS', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, }, - ], - }) + rrs: [ + { + name: 'net', + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from('foo', 'ascii'), + }, + ], + }) + ]), 'SignatureTypeMismatch' ) }) it('should reject signatures with too many labels', async function() { var instance = await dnssec.deployed() - await verifyFailedSubmission( - instance, - ...hexEncodeSignedSet({ - name: 'net', - sig: { + await expectRevert( + instance.verifyRRSet([ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'net', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 2, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'net', - type: 'TXT', + type: 'RRSIG', + ttl: 0, class: 'IN', - ttl: 3600, - data: Buffer.from('foo', 'ascii'), + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: 2, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, }, - ], - }) + rrs: [ + { + name: 'net', + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from('foo', 'ascii'), + }, + ], + }) + ]), 'InvalidLabelCount' ) }) @@ -543,943 +394,185 @@ contract('DNSSEC', function(accounts) { var instance = await dnssec.deployed() await verifySubmission( instance, - hexEncodeSignedSet({ - name: 'test', - sig: { + [ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'test', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'test', - type: 'TXT', + type: 'RRSIG', + ttl: 0, class: 'IN', - ttl: 3600, - data: Buffer.from('test', 'ascii'), + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, }, - ], - })[0], - '0x', - rootKeyProof - ) - await verifyFailedSubmission( - instance, - hexEncodeSignedSet({ - name: 'test', - sig: { + rrs: [ + { + name: 'test', + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from('test', 'ascii'), + }, + ], + }) + ] + ) + await expectRevert( + instance.verifyRRSet([ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ name: 'test', - type: 'RRSIG', - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: 'TXT', - algorithm: 253, - labels: 1, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: 'com', - signature: new Buffer([]), - }, - }, - rrs: [ - { + sig: { name: 'test', - type: 'TXT', + type: 'RRSIG', + ttl: 0, class: 'IN', - ttl: 3600, - data: Buffer.from('test', 'ascii'), + flush: false, + data: { + typeCovered: 'TXT', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: 'com', + signature: new Buffer([]), + }, }, - ], - })[0], - '0x', - rootKeyProof + rrs: [ + { + name: 'test', + type: 'TXT', + class: 'IN', + ttl: 3600, + data: Buffer.from('test', 'ascii'), + }, + ], + }) + ]), 'InvalidSignerName' ) }) it('should reject entries with expirations in the past', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() + var keys = rootKeys(expiration, inception) keys.sig.data.expiration = Date.now() / 1000 - 2 - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) + await expectRevert(instance.verifyRRSet([hexEncodeSignedSet(keys)]), 'SignatureExpired'); }) it('should reject entries with inceptions in the future', async function() { var instance = await dnssec.deployed() - var keys = rootKeys() + var keys = rootKeys(expiration, inception) keys.sig.data.inception = Date.now() / 1000 + 15 * 60 - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) - }) - - it('should accept updates with newer signatures', async function() { - var instance = await dnssec.deployed() - var keys = rootKeys() - await verifySubmission(instance, ...hexEncodeSignedSet(keys)) - }) - - it('updates the inception whether the RRs/hash have changed or not', async () => { - const instance = await dnssec.deployed() - const keys = rootKeys() - keys.sig.data.inception++ - const [signedData] = hexEncodeSignedSet(keys) - const [oldInception] = Object.values( - await instance.rrdata( - types.toType('DNSKEY'), - `0x${packet.name.encode('.').toString('hex')}` - ) - ) - assert.notEqual(oldInception, keys.sig.data.inception >>> 0) - await instance.submitRRSet( - [signedData, Buffer.alloc(0)], - anchors.encode(anchors.realEntries) - ) - const [newInception] = Object.values( - await instance.rrdata( - types.toType('DNSKEY'), - `0x${packet.name.encode('.').toString('hex')}` - ) - ) - assert.equal(newInception, keys.sig.data.inception >>> 0) - }) - - it('should reject entries that are older', async function() { - var instance = await dnssec.deployed() - var keys = rootKeys() - keys.sig.data.inception-- - await verifyFailedSubmission(instance, ...hexEncodeSignedSet(keys)) + await expectRevert(instance.verifyRRSet([hexEncodeSignedSet(keys)]), 'SignatureNotValidYet'); }) it('should reject invalid RSA signatures', async function() { var instance = await dnssec.deployed() + await instance.verifyRRSet( + [ + [test_rrsets[0][1], test_rrsets[0][2]] + ], + test_rrset_timestamp + ); var sig = test_rrsets[0][2] - await verifyFailedSubmission( - instance, - test_rrsets[0][1], - sig.slice(0, sig.length - 2) + 'FF' - ) - }) - - // Test delete RRSET - async function checkPresence(instance, type, name) { - var result = ( - await instance.rrdata.call(types.toType(type), hexEncodeName(name)) - )[2] - return result != '0x0000000000000000000000000000000000000000' - } - - function buildEntry(type, name, rrsOption, sigOption) { - var rrs = [ - { name: name, type: type, class: 'IN', ttl: 3600, data: rrsOption }, - ] - var sig = { - name: name, - type: type, - ttl: 0, - class: 'IN', - flush: false, - data: { - typeCovered: type, - algorithm: 253, - labels: name.split('.').length, - originalTTL: 3600, - expiration, - inception, - keyTag: 1278, - signersName: '.', - signature: new Buffer([]), - }, - } - - if (sigOption !== undefined) { - Object.assign(sig.data, sigOption) - } - var keys = { name, rrs, sig } - return keys - } - - async function submitEntry(instance, type, name, option, proof, sig) { - var keys = buildEntry(type, name, option, sig) - tx = await verifySubmission( - instance, - hexEncodeSignedSet(keys)[0], - '0x', - proof - ) - var res = await instance.rrdata.call( - types.toType(type), - hexEncodeName(name) - ) - assert.notEqual(res['2'], '0x0000000000000000000000000000000000000000') - return tx - } - - async function deleteEntry(instance, deletetype, deletename, nsec, proof) { - var tx, result - try { - tx = await instance.deleteRRSet( - types.toType(deletetype), - hexEncodeName(deletename), - [nsec, '0x'], - proof - ) - } catch (error) { - result = false - } - // Assert geth failed transaction - if (tx !== undefined) { - result = tx.receipt.status - } - return result - } - - async function deleteEntryNSEC3( - instance, - deletetype, - deletename, - closestEncloser, - nextClosest, - dnskey - ) { - var tx, result - try { - tx = await instance.deleteRRSetNSEC3( - types.toType(deletetype), - hexEncodeName(deletename), - [closestEncloser, '0x'], - [nextClosest, '0x'], - dnskey - ) - } catch (error) { - result = false - } - // Assert geth failed transaction - if (tx !== undefined) { - result = tx.receipt.status - } - return result - } - - it('rejects if a proof with the wrong type is supplied', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'b', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // Submit with a proof for an irrelevant record. - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'b', - hexEncodeSignedSet(rootKeys())[0], - rootKeyProof + await expectRevert( + instance.verifyRRSet( + [[test_rrsets[0][1], sig.slice(0, sig.length - 2) + 'FF']], + test_rrset_timestamp ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'b'), true) + 'NoMatchingProof' + ); }) - it('rejects if next record does not come before the deleting name', async function() { + it('should reject DS proofs with the wrong name', async function() { var instance = await dnssec.deployed() - // text z. comes after next d. - await submitEntry( - instance, - 'TXT', - 'z', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'a', { nextDomain: 'd', rrtypes: ['TXT'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'z', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'z'), true) - }) - - it('rejects if nsec record starts after the deleting name', async function() { - var instance = await dnssec.deployed() - // text a. comes after nsec b. - await submitEntry( - instance, - 'TXT', - 'a', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'b', { nextDomain: 'd', rrtypes: ['TXT'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'a', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'a'), true) - }) - - it('rejects RRset if trying to delete rrset that is in the type bitmap', async function() { - var instance = await dnssec.deployed() - // text a. has same nsec a. with type bitmap - await submitEntry( - instance, - 'TXT', - 'a', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'a', { nextDomain: 'd', rrtypes: ['TXT'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'a', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'a'), true) - }) - - it('deletes RRset if nsec name and delete name are the same but with different rrtypes', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'a', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // This test fails if rrtypes is empty ([]), but would that case every happen? - var nsec = buildEntry('NSEC', 'a', { nextDomain: 'd', rrtypes: ['NSEC'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'a', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'a'), false) - }) - - it('rejects if the proof hash does not match', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'a', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'a', { nextDomain: 'd', rrtypes: ['NSEC'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'a', - hexEncodeSignedSet(nsec)[0] + '00', - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'a'), true) - }) - - it('deletes RRset if NSEC next comes after delete name', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'b', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'a', { nextDomain: 'd', rrtypes: ['TXT'] }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'b', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'b'), false) - }) - - it('deletes RRset if NSEC is on apex domain', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'b.test', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'test', { - nextDomain: 'd.test', - rrtypes: ['TXT'], - }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'b.test', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'b.test'), false) - }) - - it('deletes RRset if NSEC next name is on apex domain', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'b.test', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry('NSEC', 'a.test', { - nextDomain: 'test', - rrtypes: ['TXT'], - }) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'b.test', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'b.test'), false) - }) - - it('will not delete a record if it is more recent than the NSEC record', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'y', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec = buildEntry( - 'NSEC', - 'x', - { nextDomain: 'z', rrtypes: ['TXT'] }, - { inception: inception - 1 } - ) - assert.equal( - await deleteEntry( - instance, - 'TXT', - 'y', - hexEncodeSignedSet(nsec)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'y'), true) - }) - - // 088vbc61o9hm3qfu7vhd3ajtilp4bc5l - // H(c.matoken.xyz) = AU8F3CT3TLG5MGO0GVKUPU95OLIH1PED - // H(matoken.xyz) = BST4HLJE7R0O8C8P4O8Q582LM0EJMIQT - // H(quux.matoken.xyz) = GJJKN49ONDFJC1THSKA8AI4CSJ8PD7AF - // l54nruaka4b4f3mfm5scv7aocqls84gm - // H(foo.matoken.xyz) = NVLH0AJQL16JP0BIGVM9JCMM50C3F8GJ - // H(_abc.matoken.xyz) = Q116RONFPGILOUJS07UEB829E1RJG0PA - - it('deletes records matching an NSEC3 record with non-matching RRTYPE', async function() { - var instance = await dnssec.deployed() - - await submitEntry( - instance, - 'TXT', - 'matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var closestEncloser = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['DNSKEY'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'matoken.xyz', - hexEncodeSignedSet(closestEncloser)[0], - '0x', - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'matoken.xyz'), false) - }) - - it('does not delete records given a matching NSEC3 on a different domain', async function() { - var instance = await dnssec.deployed() - - await submitEntry( - instance, - 'TXT', - 'matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec3 = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.example.com', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['DNSKEY'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'matoken.xyz', - hexEncodeSignedSet(nsec3)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'matoken.xyz'), true) - }) - - it('deletes records covered by an NSEC3 record', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'quux.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // In this case the same record is both closest encloser (matches the nearest existing parent domain) - // and next closest (covers the missing domain in its range). - var nsec3 = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['TXT'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'quux.matoken.xyz', - hexEncodeSignedSet(nsec3)[0], - hexEncodeSignedSet(nsec3)[0], - rootKeyProof - ), - true - ) - assert.equal( - await checkPresence(instance, 'TXT', 'quux.matoken.xyz'), - false - ) - }) - - it('deletes records at the end of a zone using NSEC3', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'foo.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var closestEncloser = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['TXT'], - } - ) - var nextClosest = buildEntry( - 'NSEC3', - 'l54nruaka4b4f3mfm5scv7aocqls84gm.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('088VBC61O9HM3QFU7VHD3AJTILP4BC5L') - ), - rrtypes: ['TXT'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'foo.matoken.xyz', - hexEncodeSignedSet(closestEncloser)[0], - hexEncodeSignedSet(nextClosest)[0], - rootKeyProof - ), - true - ) - assert.equal(await checkPresence(instance, 'TXT', 'foo.matoken.xyz'), false) - }) - - // H('b.xyz') => 71IK7JP2UN5VK1T4VM6O0E1BTUM2Q5DO - // H('matoken.xyz') => BST4HLJE7R0O8C8P4O8Q582LM0EJMIQT - // H('xyz') => CSH8K95CMN32I2NP68GMFKRBR8KFJ8HT - // H('a.xyz') => SQP040R4NN1D9S42L2BF2845DL23T4D5 - it('deletes records given a covering NSEC3 proof in a parent zone', async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'quux.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var closestEncloser = buildEntry( - 'NSEC3', - 'csh8k95cmn32i2np68gmfkrbr8kfj8ht.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('71IK7JP2UN5VK1T4VM6O0E1BTUM2Q5DO') - ), - rrtypes: ['SOA'], - } - ) - var nextClosest = buildEntry( - 'NSEC3', - '71ik7jp2un5vk1t4vm6o0e1btum2q5do.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('CCCCSKB6D83AV74TPHLHI6BGDSNTNSDJ') - ), - rrtypes: ['SOA', 'NS', 'DS'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'quux.matoken.xyz', - hexEncodeSignedSet(closestEncloser)[0], - hexEncodeSignedSet(nextClosest)[0], - rootKeyProof - ), - true - ) - assert.equal( - await checkPresence(instance, 'TXT', 'quux.matoken.xyz'), - false - ) - }) - - it("doesn't delete records given an NSEC3 covering proof in parent zone with a DS", async function() { - var instance = await dnssec.deployed() - - await submitEntry( - instance, - 'TXT', - 'quux.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var closestEncloser = buildEntry( - 'NSEC3', - 'ccccskb6d83av74tphlhi6bgdsntnsdj.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('71IK7JP2UN5VK1T4VM6O0E1BTUM2Q5DO') - ), - rrtypes: ['SOA'], - } - ) - var nextClosest = buildEntry( - 'NSEC3', - '71ik7jp2un5vk1t4vm6o0e1btum2q5do.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('CCCCSKB6D83AV74TPHLHI6BGDSNTNSDJ') - ), - rrtypes: ['SOA', 'NS', 'DS'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'quux.matoken.xyz', - hexEncodeSignedSet(closestEncloser)[0], - hexEncodeSignedSet(nextClosest)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'quux.matoken.xyz'), true) - }) - - it("doesn't delete records before the range using NSEC3", async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'c.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // In this case the same record is both closest encloser (matches the nearest existing parent domain) - // and next closest (covers the missing domain in its range). - var nsec3 = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['TXT'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'c.matoken.xyz', - hexEncodeSignedSet(nsec3)[0], - hexEncodeSignedSet(nsec3)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'c.matoken.xyz'), true) - }) - - it("doesn't delete records after the range using NSEC3", async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'foo.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // In this case the same record is both closest encloser (matches the nearest existing parent domain) - // and next closest (covers the missing domain in its range). - var nsec3 = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['TXT'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'foo.matoken.xyz', - hexEncodeSignedSet(nsec3)[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'foo.matoken.xyz'), true) - }) - it("doesn't delete nsec3 records with invalid inception", async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'quux.matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var nsec3 = buildEntry( - 'NSEC3', - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.matoken.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['TXT'], - } - ) - // Set invalid inception date - nsec3.sig.data.inception = 1 - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'quux.matoken.xyz', - hexEncodeSignedSet(nsec3)[0], - hexEncodeSignedSet(nsec3)[0], - rootKeyProof - ), - false - ) - }) - - it("doesn't delete nsec3 records with wrong data type", async function() { - var instance = await dnssec.deployed() - await submitEntry( - instance, - 'TXT', - 'b', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - // Submit with a proof for an irrelevant record. - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'b', - hexEncodeSignedSet(rootKeys())[0], - hexEncodeSignedSet(rootKeys())[0], - rootKeyProof - ), - false - ) - assert.equal(await checkPresence(instance, 'TXT', 'b'), true) - }) - - it("doesn't delete records matching an NSEC3 record with non-matching NSEC3 owner name", async function() { - var instance = await dnssec.deployed() - - await submitEntry( - instance, - 'TXT', - 'matoken.xyz', - Buffer.from('foo', 'ascii'), - rootKeyProof - ) - var closestEncloser = buildEntry( - 'NSEC3', - // Use foo.xyz instead of matoken.xyz - 'bst4hlje7r0o8c8p4o8q582lm0ejmiqt.foo.xyz', - { - algorithm: 1, - flags: 0, - iterations: 1, - salt: Buffer.from('5BA6AD4385844262', 'hex'), - nextDomain: Buffer.from( - base32hex.parse('L54NRUAKA4B4F3MFM5SCV7AOCQLS84GM') - ), - rrtypes: ['DNSKEY'], - } - ) - assert.equal( - await deleteEntryNSEC3( - instance, - 'TXT', - 'matoken.xyz', - hexEncodeSignedSet(closestEncloser)[0], - '0x', - rootKeyProof - ), - false + await expectRevert( + instance.verifyRRSet([ + hexEncodeSignedSet(rootKeys(expiration, inception)), + hexEncodeSignedSet({ + name: 'test', + sig: { + name: 'test', + type: 'RRSIG', + ttl: 0, + class: 'IN', + flush: false, + data: { + typeCovered: 'DS', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, + }, + rrs: [ + { + name: 'test', + type: 'DS', + class: 'IN', + ttl: 3600, + data: { + keyTag: 1278, // Empty body, flags == 0x0101, algorithm = 253, body = 0x0000 + algorithm: 253, + digestType: 253, + digest: new Buffer('', 'hex') + } + }, + ], + }), + hexEncodeSignedSet({ + name: 'foo', + sig: { + name: 'foo', + type: 'RRSIG', + ttl: 0, + class: 'IN', + flush: false, + data: { + typeCovered: 'DNSKEY', + algorithm: 253, + labels: 1, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: 'foo', + signature: new Buffer([]), + }, + }, + rrs: [ + { + name: 'foo', + type: 'DNSKEY', + class: 'IN', + ttl: 3600, + data: { + flags: 0x0101, + algorithm: 253, + key: Buffer.from('0000', 'HEX'), + }, + } + ] + }) + ]), + 'ProofNameMismatch' ) - assert.equal(await checkPresence(instance, 'TXT', 'matoken.xyz'), true) }) }) diff --git a/test/dnssec-oracle/TestRRUtils.sol b/test/dnssec-oracle/TestRRUtils.sol index 1edf1823..52a07699 100644 --- a/test/dnssec-oracle/TestRRUtils.sol +++ b/test/dnssec-oracle/TestRRUtils.sol @@ -12,7 +12,6 @@ contract TestRRUtils { uint16 constant DNSTYPE_MX = 15; uint16 constant DNSTYPE_TEXT = 16; uint16 constant DNSTYPE_RRSIG = 46; - uint16 constant DNSTYPE_NSEC = 47; uint16 constant DNSTYPE_TYPE1234 = 1234; function testNameLength() public pure { @@ -46,31 +45,6 @@ contract TestRRUtils { require(i == 2, "Expected 2 records"); } - function testCheckTypeBitmapTextType() public pure { - bytes memory tb = hex'0003000080'; - require(tb.checkTypeBitmap(0, DNSTYPE_TEXT) == true, "A record should exist in type bitmap"); - } - - function testCheckTypeBitmap() public pure { - // From https://tools.ietf.org/html/rfc4034#section-4.3 - // alfa.example.com. 86400 IN NSEC host.example.com. ( - // A MX RRSIG NSEC TYPE1234 - bytes memory tb = hex'FF0006400100000003041b000000000000000000000000000000000000000000000000000020'; - - // Exists in bitmap - require(tb.checkTypeBitmap(1, DNSTYPE_A) == true, "A record should exist in type bitmap"); - // Does not exist, but in a window that is included - require(tb.checkTypeBitmap(1, DNSTYPE_CNAME) == false, "CNAME record should not exist in type bitmap"); - // Does not exist, past the end of a window that is included - require(tb.checkTypeBitmap(1, 64) == false, "Type 64 should not exist in type bitmap"); - // Does not exist, in a window that does not exist - require(tb.checkTypeBitmap(1, 769) == false, "Type 769 should not exist in type bitmap"); - // Exists in a subsequent window - require(tb.checkTypeBitmap(1, DNSTYPE_TYPE1234) == true, "Type 1234 should exist in type bitmap"); - // Does not exist, past the end of the bitmap windows - require(tb.checkTypeBitmap(1, 1281) == false, "Type 1281 should not exist in type bitmap"); - } - // Canonical ordering https://tools.ietf.org/html/rfc4034#section-6.1 function testCompareNames() public pure { bytes memory bthLabXyz = hex'066274686c61620378797a00'; diff --git a/test/dnssec-oracle/TestSHA1NSEC3Digest.js b/test/dnssec-oracle/TestSHA1NSEC3Digest.js deleted file mode 100644 index a29ccae3..00000000 --- a/test/dnssec-oracle/TestSHA1NSEC3Digest.js +++ /dev/null @@ -1,70 +0,0 @@ -var base32hex = require('rfc4648').base32hex; -var sha1 = artifacts.require('./nsec3digests/SHA1NSEC3Digest.sol'); - -function fromBase32(s) { - return ( - '0x' + - Buffer.from(base32hex.parse(s.toUpperCase())).toString('hex') + - '000000000000000000000000' - ); -} - -// @todo fix byte encoding of these vectors -vectors = [ - [ - '0x', - '0x', - 0, - '0xda39a3ee5e6b4b0d3255bfef95601890afd80709000000000000000000000000' - ], - [ - web3.utils.toHex('nacl'), - web3.utils.toHex('test'), - 0, - '0x68b36a28941caebfc2af818c99a8e34478d77fec000000000000000000000000' - ], - [ - web3.utils.toHex('nacl'), - web3.utils.toHex('test'), - 1, - '0x16574cbb9312cf064794482fdd1148289027db73000000000000000000000000' - ], - [ - web3.utils.toHex('nacl'), - web3.utils.toHex('test'), - 10, - '0x455370ef51d39be8efa646b807a818c7649a505e000000000000000000000000' - ], - [ - '0xaabbccdd', - web3.utils.asciiToHex('\x07example\x00'), - 12, - fromBase32('0p9mhaveqvm6t7vbl5lop2u3t2rp3tom') - ], - [ - '0xaabbccdd', - web3.utils.asciiToHex('\x01a\x07example\x00'), - 12, - fromBase32('35mthgpgcu1qg68fab165klnsnk3dpvl') - ], - [ - '0x5BA6AD4385844262', - web3.utils.asciiToHex('\x07matoken\x03xyz\x00'), - 1, - fromBase32('bst4hlje7r0o8c8p4o8q582lm0ejmiqt') - ] -]; - -contract('SHA1NSEC3Digest', function(accounts) { - for (var i = 0; i < vectors.length; i++) { - (function(i, vector) { - it('calculates test vector ' + i, async function() { - var instance = await sha1.deployed(); - assert.equal( - await instance.hash(vector[0], vector[1], vector[2]), - vector[3] - ); - }); - })(i, vectors[i]); - } -}); diff --git a/test/truffle-fixture.js b/test/truffle-fixture.js index 758211cb..b710b479 100644 --- a/test/truffle-fixture.js +++ b/test/truffle-fixture.js @@ -2,7 +2,6 @@ const RSASHA1Algorithm = artifacts.require('./algorithms/RSASHA1Algorithm'); const RSASHA256Algorithm = artifacts.require('./algorithms/RSASHA256Algorithm'); const SHA1Digest = artifacts.require('./digests/SHA1Digest'); const SHA256Digest = artifacts.require('./digests/SHA256Digest'); -const SHA1NSEC3Digest = artifacts.require('./nsec3digests/SHA1NSEC3Digest'); const DNSSEC = artifacts.require('./DNSSECImpl'); const DummyAlgorithm = artifacts.require('./algorithms/DummyAlgorithm'); const DummyDigest = artifacts.require('./digests/DummyDigest'); @@ -31,7 +30,6 @@ module.exports = async function(hre) { const rsasha1 = await deploy(RSASHA1Algorithm); const sha256 = await deploy(SHA256Digest); const sha1 = await deploy(SHA1Digest); - const nsec3sha1 = await deploy(SHA1NSEC3Digest); const curve = await deploy(EllipticCurve); const p256 = await deploy(P256SHA256Algorithm, curve.address); @@ -55,7 +53,5 @@ module.exports = async function(hre) { tasks.push(dnssec.setDigest(1, sha1.address)); tasks.push(dnssec.setDigest(2, sha256.address)); - tasks.push(dnssec.setNSEC3Digest(1, nsec3sha1.address)); - await Promise.all(tasks); }; diff --git a/test/utils/dnsutils.js b/test/utils/dnsutils.js new file mode 100644 index 00000000..6c400254 --- /dev/null +++ b/test/utils/dnsutils.js @@ -0,0 +1,63 @@ +const SignedSet = require('@ensdomains/dnsprovejs').SignedSet + +function hexEncodeSignedSet(keys) { + const ss = new SignedSet(keys.rrs, keys.sig) + return [ss.toWire(), ss.signature.data.signature] +} + +function hexEncodeName(name) { + return '0x' + packet.name.encode(name).toString('hex') +} + +function rootKeys(expiration, inception) { + var name = '.' + var sig = { + name: '.', + type: 'RRSIG', + ttl: 0, + class: 'IN', + flush: false, + data: { + typeCovered: 'DNSKEY', + algorithm: 253, + labels: 0, + originalTTL: 3600, + expiration, + inception, + keyTag: 1278, + signersName: '.', + signature: new Buffer([]), + }, + } + + var rrs = [ + { + name: '.', + type: 'DNSKEY', + class: 'IN', + ttl: 3600, + data: { flags: 0, algorithm: 253, key: Buffer.from('0000', 'HEX') }, + }, + { + name: '.', + type: 'DNSKEY', + class: 'IN', + ttl: 3600, + data: { flags: 0, algorithm: 253, key: Buffer.from('1112', 'HEX') }, + }, + { + name: '.', + type: 'DNSKEY', + class: 'IN', + ttl: 3600, + data: { + flags: 0x0101, + algorithm: 253, + key: Buffer.from('0000', 'HEX'), + }, + }, + ] + return { name, sig, rrs } +} + +module.exports = { hexEncodeName, hexEncodeSignedSet, rootKeys } diff --git a/yarn.lock b/yarn.lock index 7e44c552..979b2c40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,6 +1181,13 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@solidity-parser/parser@^0.14.3": + version "0.14.3" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.3.tgz#0d627427b35a40d8521aaa933cc3df7d07bfa36f" + integrity sha512-29g2SZ29HtsqA58pLCtopI1P/cPy5/UAzlcAXO6T/CNJimG6yA8kx4NaseMyJULiC+TEs02Y9/yeHzClqoA0hw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -3879,6 +3886,11 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5 minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emoji-regex@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66" + integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -4048,7 +4060,7 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@4.0.0: +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== @@ -8072,6 +8084,18 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier-plugin-solidity@^1.0.0-beta.24: + version "1.0.0-beta.24" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.24.tgz#67573ca87098c14f7ccff3639ddd8a4cab2a87eb" + integrity sha512-6JlV5BBTWzmDSq4kZ9PTXc3eLOX7DF5HpbqmmaF+kloyUwOZbJ12hIYsUaZh2fVgZdV2t0vWcvY6qhILhlzgqg== + dependencies: + "@solidity-parser/parser" "^0.14.3" + emoji-regex "^10.1.0" + escape-string-regexp "^4.0.0" + semver "^7.3.7" + solidity-comments-extractor "^0.0.7" + string-width "^4.2.3" + prettier@^1.14.3: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" @@ -8763,6 +8787,13 @@ semver@^7.3.4: dependencies: lru-cache "^6.0.0" +semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -9056,6 +9087,11 @@ solhint@^2.0.0: optionalDependencies: prettier "^1.14.3" +solidity-comments-extractor@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" + integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -9266,7 +9302,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==