// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; pragma experimental ABIEncoderV2; import "./Owned.sol"; import "./BytesUtils.sol"; import "./RRUtils.sol"; import "./DNSSEC.sol"; import "./algorithms/Algorithm.sol"; import "./digests/Digest.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; /* * @dev An oracle contract that verifies and stores DNSSEC-validated DNS records. */ contract DNSSECImpl is DNSSEC, Owned { using Buffer for Buffer.buffer; using BytesUtils for bytes; using RRUtils for *; uint16 constant DNSCLASS_IN = 1; uint16 constant DNSTYPE_DS = 43; uint16 constant DNSTYPE_DNSKEY = 48; uint256 constant DNSKEY_FLAG_ZONEKEY = 0x100; 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; /** * @dev Constructor. * @param _anchors The binary format RR entries for the root DS records. */ constructor(bytes memory _anchors) { // Insert the 'trust anchors' - the key hashes that start the chain // of trust for all other records. anchors = _anchors; } /** * @dev Sets the contract address for a signature verification algorithm. * Callable only by the owner. * @param id The algorithm ID * @param algo The address of the algorithm contract. */ function setAlgorithm(uint8 id, Algorithm algo) public owner_only { algorithms[id] = algo; emit AlgorithmUpdated(id, address(algo)); } /** * @dev Sets the contract address for a digest verification algorithm. * Callable only by the owner. * @param id The digest ID * @param digest The address of the digest contract. */ function setDigest(uint8 id, Digest digest) public owner_only { digests[id] = digest; emit DigestUpdated(id, address(digest)); } /** * @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 verifyRRSet(RRSetWithSignature[] memory input) external view virtual override returns (bytes memory rrs, uint32 inception) { return verifyRRSet(input, block.timestamp); } /** * @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 verifyRRSet(RRSetWithSignature[] memory input, uint256 now) public view virtual override returns (bytes memory rrs, uint32 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; } return (proof, inception); } /** * @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. * @param now The current timestamp. */ function validateSignedSet( RRSetWithSignature memory input, bytes memory proof, uint256 now ) internal view returns (RRUtils.SignedSet memory rrset) { rrset = input.rrset.readSignedSet(); // Do some basic checks on the RRs and extract the name bytes memory name = validateRRs(rrset, rrset.typeCovered); if (name.labelCount(0) != rrset.labels) { revert InvalidLabelCount(name, rrset.labels); } rrset.name = name; // All comparisons involving the Signature Expiration and // Inception fields MUST use "serial number arithmetic", as // defined in RFC 1982 // 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. 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. if (!RRUtils.serialNumberGte(uint32(now), rrset.inception)) { revert SignatureNotValidYet(rrset.inception, uint32(now)); } // Validate the signature verifySignature(name, rrset, input, proof); return rrset; } /** * @dev Validates a set of RRs. * @param rrset The RR set. * @param typecovered The type covered by the RRSIG record. */ function validateRRs(RRUtils.SignedSet memory rrset, uint16 typecovered) internal pure returns (bytes memory name) { // Iterate over all the RRs for ( RRUtils.RRIterator memory iter = rrset.rrs(); !iter.done(); iter.next() ) { // We only support class IN (Internet) 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. 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. if (iter.dnstype != typecovered) { revert SignatureTypeMismatch(iter.dnstype, typecovered); } } } /** * @dev Performs signature verification. * * Throws or reverts if unable to verify the record. * * @param name The name of the RRSIG record, in DNS label-sequence format. * @param data The original data to verify. * @param proof A DS or DNSKEY record that's already verified by the oracle. */ function verifySignature( bytes memory name, RRUtils.SignedSet memory rrset, RRSetWithSignature memory data, bytes memory proof ) internal view { // o The RRSIG RR's Signer's Name field MUST be the name of the zone // that contains the RRset. 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) { verifyWithDS(rrset, data, proofRR); } else if (proofRR.dnstype == DNSTYPE_DNSKEY) { verifyWithKnownKey(rrset, data, proofRR); } else { revert InvalidProofType(proofRR.dnstype); } } /** * @dev Attempts to verify a signed RRSET against an already known public key. * @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. */ function verifyWithKnownKey( RRUtils.SignedSet memory rrset, RRSetWithSignature memory data, RRUtils.RRIterator memory proof ) internal view { // Check the DNSKEY's owner name matches the signer name on the RRSIG for (; !proof.done(); proof.next()) { 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; } } revert NoMatchingProof(rrset.signerName); } /** * @dev Attempts to verify some data using a provided key and a signature. * @param dnskey The dns key record to verify the signature with. * @param rrset The signed RRSET being verified. * @param data The original data `rrset` was decoded from. * @return True iff the key verifies the signature. */ function verifySignatureWithKey( RRUtils.DNSKEY memory dnskey, bytes memory keyrdata, RRUtils.SignedSet memory rrset, RRSetWithSignature memory data ) internal view returns (bool) { // TODO: Check key isn't expired, unless updating key itself // The Protocol Field MUST have value 3 (RFC4034 2.1.2) if (dnskey.protocol != 3) { return false; } // o The RRSIG RR's Signer's Name, Algorithm, and Key Tag fields MUST // match the owner name, algorithm, and key tag for some DNSKEY RR in // the zone's apex DNSKEY RRset. if (dnskey.algorithm != rrset.algorithm) { return false; } uint16 computedkeytag = keyrdata.computeKeytag(); if (computedkeytag != rrset.keytag) { return false; } // o The matching DNSKEY RR MUST be present in the zone's apex DNSKEY // RRset, and MUST have the Zone Flag bit (DNSKEY RDATA Flag bit 7) // set. if (dnskey.flags & DNSKEY_FLAG_ZONEKEY == 0) { return false; } return algorithms[dnskey.algorithm].verify(keyrdata, data.rrset, data.sig); } /** * @dev Attempts to verify a signed RRSET against an already known hash. This function assumes * that the record * @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. */ function verifyWithDS( RRUtils.SignedSet memory rrset, RRSetWithSignature memory data, RRUtils.RRIterator memory proof ) internal view { for ( RRUtils.RRIterator memory iter = rrset.rrs(); !iter.done(); iter.next() ) { if (iter.dnstype != DNSTYPE_DNSKEY) { revert InvalidProofType(iter.dnstype); } bytes memory keyrdata = iter.rdata(); RRUtils.DNSKEY memory dnskey = keyrdata.readDNSKEY( 0, keyrdata.length ); if (verifySignatureWithKey(dnskey, keyrdata, rrset, data)) { // It's self-signed - look for a DS record to verify it. if ( verifyKeyWithDS(rrset.signerName, proof, dnskey, keyrdata) ) { return; } } } revert NoMatchingProof(rrset.signerName); } /** * @dev Attempts to verify a key using DS records. * @param keyname The DNS name of the key, in DNS label-sequence format. * @param dsrrs The DS records to use in verification. * @param dnskey The dnskey to verify. * @param keyrdata The RDATA section of the key. * @return True if a DS record verifies this key. */ function verifyKeyWithDS( bytes memory keyname, RRUtils.RRIterator memory dsrrs, RRUtils.DNSKEY memory dnskey, bytes memory keyrdata ) 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 ); if (ds.keytag != keytag) { continue; } if (ds.algorithm != dnskey.algorithm) { continue; } Buffer.buffer memory buf; buf.init(keyname.length + keyrdata.length); buf.append(keyname); buf.append(keyrdata); if (verifyDSHash(ds.digestType, buf.buf, ds.digest)) { return true; } } return false; } /** * @dev Attempts to verify a DS record's hash value against some data. * @param digesttype The digest ID from the DS record. * @param data The data to digest. * @param digest The digest data to check against. * @return True iff the digest matches. */ function verifyDSHash( uint8 digesttype, bytes memory data, bytes memory digest ) internal view returns (bool) { if (address(digests[digesttype]) == address(0)) { return false; } return digests[digesttype].verify(data, digest); } }