// SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) package org.xbill.DNS; import java.security.GeneralSecurityException; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.utils.base64; import org.xbill.DNS.utils.hexdump; /** * Transaction signature handling. This class generates and verifies TSIG records on messages, which * provide transaction security. * * @see TSIGRecord * @author Brian Wellington */ @Slf4j public class TSIG { // https://www.iana.org/assignments/tsig-algorithm-names/tsig-algorithm-names.xml /** * The domain name representing the gss-tsig algorithm. * * @see RFC 3645 */ public static final Name GSS_TSIG = Name.fromConstantString("gss-tsig."); /** * The domain name representing the HMAC-MD5 algorithm. * * @see RFC 8945 */ public static final Name HMAC_MD5 = Name.fromConstantString("HMAC-MD5.SIG-ALG.REG.INT."); /** * The domain name representing the HMAC-MD5 algorithm. * * @see RFC 8945 * @deprecated use {@link #HMAC_MD5} */ @Deprecated public static final Name HMAC = HMAC_MD5; /** * The domain name representing the HMAC-SHA1 algorithm. * * @see RFC 8945 */ public static final Name HMAC_SHA1 = Name.fromConstantString("hmac-sha1."); /** * The domain name representing the HMAC-SHA224 algorithm. * * @see RFC 8945 */ public static final Name HMAC_SHA224 = Name.fromConstantString("hmac-sha224."); /** * The domain name representing the HMAC-SHA256 algorithm. * * @see RFC 8945 */ public static final Name HMAC_SHA256 = Name.fromConstantString("hmac-sha256."); /** * The domain name representing the HMAC-SHA384 algorithm. * * @see RFC 8945 */ public static final Name HMAC_SHA384 = Name.fromConstantString("hmac-sha384."); /** * The domain name representing the HMAC-SHA512 algorithm. * * @see RFC 8945 */ public static final Name HMAC_SHA512 = Name.fromConstantString("hmac-sha512."); /** * The domain name representing the HMAC-SHA256-128 algorithm. * * @see RFC 4868 * @see RFC 8945 * @since 3.6.3 */ public static final Name HMAC_SHA256_128 = Name.fromConstantString("hmac-sha256-128."); /** * The domain name representing the HMAC-SHA384-192 algorithm. * * @see RFC 4868 * @see RFC 8945 * @since 3.6.3 */ public static final Name HMAC_SHA384_192 = Name.fromConstantString("hmac-sha384-192."); /** * The domain name representing the HMAC-SHA512-256 algorithm. * * @see RFC 4868 * @see RFC 8945 * @since 3.6.3 */ public static final Name HMAC_SHA512_256 = Name.fromConstantString("hmac-sha512-256."); private static final Map algMap; private static final Map algLengthMap; private static final Pattern javaAlgNamePattern = Pattern.compile( "^Hmac(?(SHA(1|\\d{3})|MD5))(/(?\\d{3}))?$", Pattern.CASE_INSENSITIVE); static { Map names = new TreeMap<>(); names.put(HMAC_MD5, "HmacMD5"); names.put(HMAC_SHA1, "HmacSHA1"); names.put(HMAC_SHA224, "HmacSHA224"); names.put(HMAC_SHA256, "HmacSHA256"); names.put(HMAC_SHA384, "HmacSHA384"); names.put(HMAC_SHA512, "HmacSHA512"); // These must always be after the non-truncated versions names.put(HMAC_SHA256_128, "HmacSHA256"); names.put(HMAC_SHA384_192, "HmacSHA384"); names.put(HMAC_SHA512_256, "HmacSHA512"); algMap = Collections.unmodifiableMap(names); Map lengths = new HashMap<>(); lengths.put(HMAC_MD5, 16); lengths.put(HMAC_SHA1, 20); lengths.put(HMAC_SHA224, 28); lengths.put(HMAC_SHA256, 32); lengths.put(HMAC_SHA384, 48); lengths.put(HMAC_SHA512, 64); lengths.put(HMAC_SHA256_128, 16); lengths.put(HMAC_SHA384_192, 24); lengths.put(HMAC_SHA512_256, 32); algLengthMap = Collections.unmodifiableMap(lengths); } /** * Convert an algorithm String to its equivalent Name. * * @param alg String containing name of algorithm. * @return Name object for algorithm * @throws IllegalArgumentException The algorithm is null or invalid. */ public static Name algorithmToName(String alg) { if (alg == null) { throw new IllegalArgumentException("Null algorithm"); } // Handle Java algorithm names if (!alg.contains("-")) { Matcher m = javaAlgNamePattern.matcher(alg); if (m.matches()) { alg = "hmac-" + m.group("alg"); String truncatedLength = m.group("length"); if (truncatedLength != null) { alg += "-" + truncatedLength; } } } if (!alg.endsWith(".")) { alg += "."; } Name nameAlg; try { nameAlg = Name.fromString(alg); } catch (TextParseException e) { throw new IllegalArgumentException(e); } // Special case, allow "hmac-md5" as an alias for the RFC name. if (nameAlg.equals(Name.fromConstantString("hmac-md5."))) { return HMAC_MD5; } // Make sure we understand this name if (algMap.get(nameAlg) == null) { throw new IllegalArgumentException("Unknown algorithm: " + nameAlg); } return nameAlg; } /** * Convert an algorithm Name to a string. * * @param name Name object * @return String equivalent * @deprecated Returns java algorithm name, will be made private in 4.0 */ @Deprecated public static String nameToAlgorithm(Name name) { String alg = algMap.get(name); if (alg != null) { return alg; } throw new IllegalArgumentException("Unknown algorithm: " + name); } /** The default fudge value for outgoing packets. Can be overridden by the tsigfudge option. */ public static final Duration FUDGE = Duration.ofSeconds(300); private final Name alg; private final Clock clock; private final Name name; private final SecretKey macKey; private final String macAlgorithm; private final Mac sharedHmac; /** * Verifies the data (computes the secure hash and compares it to the input) * * @param expected The expected (locally calculated) signature * @param signature The signature to compare against * @return true if the signature matches, false otherwise */ private static boolean verify(byte[] expected, byte[] signature) { if (signature.length < expected.length) { byte[] truncated = new byte[signature.length]; System.arraycopy(expected, 0, truncated, 0, truncated.length); expected = truncated; } return Arrays.equals(signature, expected); } private Mac initHmac() { if (sharedHmac != null) { try { return (Mac) sharedHmac.clone(); } catch (CloneNotSupportedException e) { sharedHmac.reset(); return sharedHmac; } } try { Mac mac = Mac.getInstance(macAlgorithm); mac.init(macKey); return mac; } catch (GeneralSecurityException ex) { throw new IllegalArgumentException("Caught security exception setting up HMAC.", ex); } } /** * Creates a new TSIG object, which can be used to sign or verify a message. * * @param name The name of the shared key. * @param key The shared key's data represented as a base64 encoded string. * @throws IllegalArgumentException The key name is an invalid name * @throws IllegalArgumentException The key data is improperly encoded * @throws NullPointerException key is null * @since 3.2 */ public TSIG(Name algorithm, Name name, String key) { this(algorithm, name, Objects.requireNonNull(base64.fromString(key))); } /** * Creates a new TSIG key, which can be used to sign or verify a message. * * @param algorithm The algorithm of the shared key. * @param name The name of the shared key. * @param keyBytes The shared key's data. */ public TSIG(Name algorithm, Name name, byte[] keyBytes) { this(algorithm, name, new SecretKeySpec(keyBytes, nameToAlgorithm(algorithm))); } /** * Creates a new TSIG key, which can be used to sign or verify a message. * * @param algorithm The algorithm of the shared key. * @param name The name of the shared key. * @param key The shared key. */ public TSIG(Name algorithm, Name name, SecretKey key) { this(algorithm, name, key, Clock.systemUTC()); } /** * Creates a new TSIG key, which can be used to sign or verify a message. * * @param algorithm The algorithm of the shared key. * @param name The name of the shared key. * @param key The shared key. * @since 3.2 */ public TSIG(Name algorithm, Name name, SecretKey key, Clock clock) { this.name = name; this.alg = algorithm; this.clock = clock; this.macAlgorithm = nameToAlgorithm(algorithm); this.macKey = key; this.sharedHmac = null; } /** * Creates a new TSIG key from a pre-initialized Mac instance. This assumes that init() has * already been called on the mac to set up the key. * * @param mac The JCE HMAC object * @param name The name of the key * @deprecated Use one of the constructors that specifies an algorithm and key. */ @Deprecated public TSIG(Mac mac, Name name) { this.name = name; this.sharedHmac = mac; this.macAlgorithm = null; this.macKey = null; this.clock = Clock.systemUTC(); this.alg = algorithmToName(mac.getAlgorithm()); } /** * Creates a new TSIG key with the {@link #HMAC_MD5} algorithm, which can be used to sign or * verify a message. * * @param name The name of the shared key. * @param key The shared key's data. * @deprecated Use {@link #TSIG(Name, Name, SecretKey)} to explicitly specify an algorithm. */ @Deprecated public TSIG(Name name, byte[] key) { this(HMAC_MD5, name, key); } /** * Creates a new TSIG object, which can be used to sign or verify a message. * * @param name The name of the shared key. * @param key The shared key's data represented as a base64 encoded string. * @throws IllegalArgumentException The key name is an invalid name * @throws IllegalArgumentException The key data is improperly encoded */ public TSIG(Name algorithm, String name, String key) { byte[] keyBytes = base64.fromString(key); if (keyBytes == null) { throw new IllegalArgumentException("Invalid TSIG key string"); } try { this.name = Name.fromString(name, Name.root); } catch (TextParseException e) { throw new IllegalArgumentException("Invalid TSIG key name"); } this.alg = algorithm; this.clock = Clock.systemUTC(); this.macAlgorithm = nameToAlgorithm(algorithm); this.sharedHmac = null; this.macKey = new SecretKeySpec(keyBytes, macAlgorithm); } /** * Creates a new TSIG object, which can be used to sign or verify a message. * * @param algorithm The RFC8945 algorithm name of the shared key. The legal values are: *
    *
  • hmac-md5.sig-alg.reg.int. *
  • hmac-md5. (alias for hmac-md5.sig-alg.reg.int.) *
  • hmac-sha1. *
  • hmac-sha224. *
  • hmac-sha256. *
  • hmac-sha256-128. *
  • hmac-sha384. *
  • hmac-sha384-192. *
  • hmac-sha512. *
  • hmac-sha512-256. *
* The trailing "." can be omitted. * @param name The name of the shared key. * @param key The shared key's data represented as a base64 encoded string. * @throws IllegalArgumentException The key name is an invalid name * @throws IllegalArgumentException The key data is improperly encoded * @see RFC 8945 * @apiNote Do NOT use the MD5 algorithms anymore. */ public TSIG(String algorithm, String name, String key) { this(algorithmToName(algorithm), name, key); } /** * Creates a new TSIG object with the {@link #HMAC_MD5} algorithm, which can be used to sign or * verify a message. * * @param name The name of the shared key * @param key The shared key's data, represented as a base64 encoded string. * @throws IllegalArgumentException The key name is an invalid name * @throws IllegalArgumentException The key data is improperly encoded * @deprecated Use {@link #TSIG(Name, String, String)} to explicitly specify an algorithm. */ @Deprecated public TSIG(String name, String key) { this(HMAC_MD5, name, key); } /** * Creates a new TSIG object, which can be used to sign or verify a message. * * @param str The TSIG key, in the form name:secret, name/secret, alg:name:secret, or * alg/name/secret. If no algorithm is specified, the default of {@link #HMAC_MD5} is used. * @throws IllegalArgumentException The string does not contain both a name and secret. * @throws IllegalArgumentException The key name is an invalid name * @throws IllegalArgumentException The key data is improperly encoded * @deprecated Use an explicit constructor */ @Deprecated public static TSIG fromString(String str) { String[] parts = str.split("[:/]", 3); switch (parts.length) { case 2: return new TSIG(HMAC_MD5, parts[0], parts[1]); case 3: return new TSIG(parts[0], parts[1], parts[2]); default: throw new IllegalArgumentException("Invalid TSIG key specification"); } } /** * Generates a TSIG record with a specific error for a message that has been rendered. * * @param m The message * @param b The rendered message * @param error The error * @param old If this message is a response, the TSIG from the request * @return The TSIG record to be added to the message */ public TSIGRecord generate(Message m, byte[] b, int error, TSIGRecord old) { return generate(m, b, error, old, true); } /** * Generates a TSIG record with a specific error for a message that has been rendered. * * @param m The message * @param b The rendered message * @param error The error * @param old If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this {@link TSIGRecord} is the to be added to the first of * many messages in a TCP connection and all TSIG variables (rfc2845, 3.4.2.) should be * included in the signature. {@code false} for subsequent messages with reduced TSIG * variables set (rfc2845, 4.4.). * @return The TSIG record to be added to the message * @since 3.2 */ public TSIGRecord generate( Message m, byte[] b, int error, TSIGRecord old, boolean fullSignature) { Mac hmac = null; if (error == Rcode.NOERROR || error == Rcode.BADTIME || error == Rcode.BADTRUNC) { hmac = initHmac(); } return generate(m, b, error, old, fullSignature, hmac); } /** * Generates a TSIG record with a specific error for a message that has been rendered. * * @param m The message * @param b The rendered message * @param error The error * @param old If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this {@link TSIGRecord} is the to be added to the first of * many messages in a TCP connection and all TSIG variables (rfc2845, 3.4.2.) should be * included in the signature. {@code false} for subsequent messages with reduced TSIG * variables set (rfc2845, 4.4.). * @param hmac A mac instance to reuse for a stream of messages to sign, e.g. when doing a zone * transfer. * @return The TSIG record to be added to the message */ private TSIGRecord generate( Message m, byte[] b, int error, TSIGRecord old, boolean fullSignature, Mac hmac) { Instant timeSigned = getTimeSigned(error, old); Duration fudge = getTsigFudge(); boolean signing = hmac != null; if (old != null && signing) { hmacAddSignature(hmac, old); } // Digest the message if (signing) { if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC rendered message", b)); } hmac.update(b); } // rfc2845, 3.4.2 TSIG Variables // for section 4.4 TSIG on TCP connection: skip name, class, ttl, alg and other DNSOutput out = new DNSOutput(); if (fullSignature) { name.toWireCanonical(out); out.writeU16(DClass.ANY); /* class */ out.writeU32(0); /* ttl */ alg.toWireCanonical(out); } writeTsigTimerVariables(timeSigned, fudge, out); if (fullSignature) { out.writeU16(error); out.writeU16(0); /* No other data */ } byte[] signature; if (signing) { byte[] tsigVariables = out.toByteArray(); if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC variables", tsigVariables)); } signature = hmac.doFinal(tsigVariables); if (signature.length > algLengthMap.get(alg)) { signature = Arrays.copyOfRange(signature, 0, algLengthMap.get(alg)); } } else { signature = new byte[0]; } byte[] other = null; if (error == Rcode.BADTIME) { out = new DNSOutput(6); writeTsigTime(clock.instant(), out); other = out.toByteArray(); } return new TSIGRecord( name, DClass.ANY, 0, alg, timeSigned, fudge, signature, m.getHeader().getID(), error, other); } private Instant getTimeSigned(int error, TSIGRecord old) { return error == Rcode.BADTIME ? old.getTimeSigned() : clock.instant(); } private static Duration getTsigFudge() { int fudgeOption = Options.intValue("tsigfudge"); return fudgeOption < 0 || fudgeOption > 0x7FFF ? FUDGE : Duration.ofSeconds(fudgeOption); } /** * Generates a TSIG record for a message and adds it to the message * * @param m The message * @param old If this message is a response, the TSIG from the request */ public void apply(Message m, TSIGRecord old) { apply(m, Rcode.NOERROR, old, true); } /** * Generates a TSIG record with a specific error for a message and adds it to the message. * * @param m The message * @param error The error * @param old If this message is a response, the TSIG from the request */ public void apply(Message m, int error, TSIGRecord old) { apply(m, error, old, true); } /** * Generates a TSIG record with a specific error for a message and adds it to the message. * * @param m The message * @param old If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this message is the first of many in a TCP connection and * all TSIG variables (rfc2845, 3.4.2.) should be included in the signature. {@code false} for * subsequent messages with reduced TSIG variables set (rfc2845, 4.4.). * @since 3.2 */ public void apply(Message m, TSIGRecord old, boolean fullSignature) { apply(m, Rcode.NOERROR, old, fullSignature); } /** * Generates a TSIG record with a specific error for a message and adds it to the message. * * @param m The message * @param error The error * @param old If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this message is the first of many in a TCP connection and * all TSIG variables (rfc2845, 3.4.2.) should be included in the signature. {@code false} for * subsequent messages with reduced TSIG variables set (rfc2845, 4.4.). * @since 3.2 */ public void apply(Message m, int error, TSIGRecord old, boolean fullSignature) { Record r = generate(m, m.toWire(), error, old, fullSignature); m.addRecord(r, Section.ADDITIONAL); m.tsigState = Message.TSIG_SIGNED; } /** * Generates a TSIG record for a message and adds it to the message * * @param m The message * @param old If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this message is the first of many in a TCP connection and * all TSIG variables (rfc2845, 3.4.2.) should be included in the signature. {@code false} for * subsequent messages with reduced TSIG variables set (rfc2845, 4.4.). * @deprecated use {@link #apply(Message, TSIGRecord, boolean)} */ @Deprecated public void applyStream(Message m, TSIGRecord old, boolean fullSignature) { apply(m, Rcode.NOERROR, old, fullSignature); } /** * Verifies a TSIG record on an incoming message. Since this is only called in the context where a * TSIG is expected to be present, it is an error if one is not present. After calling this * routine, Message.isVerified() may be called on this message. * *

Use {@link StreamVerifier} to validate multiple messages in a stream. * * @param m The message * @param b An array containing the message in unparsed form. This is necessary since TSIG signs * the message in wire format, and we can't recreate the exact wire format (with the same name * compression). * @param length unused * @param old If this message is a response, the TSIG from the request * @return The result of the verification (as an Rcode) * @see Rcode * @deprecated use {@link #verify(Message, byte[], TSIGRecord)} */ @Deprecated public byte verify(Message m, byte[] b, int length, TSIGRecord old) { return (byte) verify(m, b, old); } /** * Verifies a TSIG record on an incoming message. Since this is only called in the context where a * TSIG is expected to be present, it is an error if one is not present. After calling this * routine, Message.isVerified() may be called on this message. * *

Use {@link StreamVerifier} to validate multiple messages in a stream. * * @param m The message to verify * @param messageBytes An array containing the message in unparsed form. This is necessary since * TSIG signs the message in wire format, and we can't recreate the exact wire format (with * the same name compression). * @param requestTSIG If this message is a response, the TSIG from the request * @return The result of the verification (as an Rcode) * @see Rcode */ public int verify(Message m, byte[] messageBytes, TSIGRecord requestTSIG) { return verify(m, messageBytes, requestTSIG, true); } /** * Verifies a TSIG record on an incoming message. Since this is only called in the context where a * TSIG is expected to be present, it is an error if one is not present. After calling this * routine, Message.isVerified() may be called on this message. * *

Use {@link StreamVerifier} to validate multiple messages in a stream. * * @param m The message to verify * @param messageBytes An array containing the message in unparsed form. This is necessary since * TSIG signs the message in wire format, and we can't recreate the exact wire format (with * the same name compression). * @param requestTSIG If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this message is the first of many in a TCP connection and * all TSIG variables (rfc2845, 3.4.2.) should be included in the signature. {@code false} for * subsequent messages with reduced TSIG variables set (rfc2845, 4.4.). * @return The result of the verification (as an Rcode) * @see Rcode * @since 3.2 */ public int verify(Message m, byte[] messageBytes, TSIGRecord requestTSIG, boolean fullSignature) { return verify(m, messageBytes, requestTSIG, fullSignature, null); } /** * Verifies a TSIG record on an incoming message. Since this is only called in the context where a * TSIG is expected to be present, it is an error if one is not present. After calling this * routine, Message.isVerified() may be called on this message. * * @param m The message to verify * @param messageBytes An array containing the message in unparsed form. This is necessary since * TSIG signs the message in wire format, and we can't recreate the exact wire format (with * the same name compression). * @param requestTSIG If this message is a response, the TSIG from the request * @param fullSignature {@code true} if this message is the first of many in a TCP connection and * all TSIG variables (rfc2845, 3.4.2.) should be included in the signature. {@code false} for * subsequent messages with reduced TSIG variables set (rfc2845, 4.4.). * @return The result of the verification (as an Rcode) * @see Rcode */ private int verify( Message m, byte[] messageBytes, TSIGRecord requestTSIG, boolean fullSignature, Mac hmac) { m.tsigState = Message.TSIG_FAILED; TSIGRecord tsig = m.getTSIG(); if (tsig == null) { return Rcode.FORMERR; } if (!tsig.getName().equals(name) || !tsig.getAlgorithm().equals(alg)) { log.debug( "BADKEY failure on message id {}, expected: {}/{}, actual: {}/{}", m.getHeader().getID(), name, alg, tsig.getName(), tsig.getAlgorithm()); return Rcode.BADKEY; } if (hmac == null) { hmac = initHmac(); } if (requestTSIG != null && tsig.getError() != Rcode.BADKEY && tsig.getError() != Rcode.BADSIG) { hmacAddSignature(hmac, requestTSIG); } m.getHeader().decCount(Section.ADDITIONAL); byte[] header = m.getHeader().toWire(); m.getHeader().incCount(Section.ADDITIONAL); if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC header", header)); } hmac.update(header); int len = m.tsigstart - header.length; if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC message after header", messageBytes, header.length, len)); } hmac.update(messageBytes, header.length, len); byte[] tsigVariables = getTsigVariables(fullSignature, tsig); hmac.update(tsigVariables); byte[] signature = tsig.getSignature(); int badsig = verifySignature(hmac, signature); if (badsig != Rcode.NOERROR) { return badsig; } // validate time after the signature, as per // https://datatracker.ietf.org/doc/html/rfc8945#section-5.4 int badtime = verifyTime(tsig); if (badtime != Rcode.NOERROR) { return badtime; } m.tsigState = Message.TSIG_VERIFIED; return Rcode.NOERROR; } private static byte[] getTsigVariables(boolean fullSignature, TSIGRecord tsig) { DNSOutput out = new DNSOutput(); if (fullSignature) { tsig.getName().toWireCanonical(out); out.writeU16(tsig.dclass); out.writeU32(tsig.ttl); tsig.getAlgorithm().toWireCanonical(out); } writeTsigTimerVariables(tsig.getTimeSigned(), tsig.getFudge(), out); if (fullSignature) { out.writeU16(tsig.getError()); if (tsig.getOther() != null) { out.writeU16(tsig.getOther().length); out.writeByteArray(tsig.getOther()); } else { out.writeU16(0); } } byte[] tsigVariables = out.toByteArray(); if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC variables", tsigVariables)); } return tsigVariables; } private int verifySignature(Mac hmac, byte[] signature) { int digestLength = hmac.getMacLength(); // rfc4635#section-3.1, 4.: // "MAC size" field is less than the larger of 10 (octets) and half // the length of the hash function in use int minDigestLength = Math.max(10, digestLength / 2); if (signature.length > digestLength) { log.debug( "BADSIG: signature too long, expected: {}, actual: {}", digestLength, signature.length); return Rcode.BADSIG; } else if (signature.length < minDigestLength) { log.debug( "BADSIG: signature too short, expected: {} of {}, actual: {}", minDigestLength, digestLength, signature.length); return Rcode.BADSIG; } else { byte[] expectedSignature = hmac.doFinal(); if (expectedSignature.length > algLengthMap.get(alg)) { expectedSignature = Arrays.copyOfRange(expectedSignature, 0, algLengthMap.get(alg)); } if (!verify(expectedSignature, signature)) { if (log.isDebugEnabled()) { log.debug( "BADSIG: signature verification failed, expected: {}, actual: {}", base64.toString(expectedSignature), base64.toString(signature)); } return Rcode.BADSIG; } } return Rcode.NOERROR; } private int verifyTime(TSIGRecord tsig) { Instant now = clock.instant(); Duration delta = Duration.between(now, tsig.getTimeSigned()).abs(); if (delta.compareTo(tsig.getFudge()) > 0) { log.debug( "BADTIME failure, now {} +/- tsig {} > fudge {}", now, tsig.getTimeSigned(), tsig.getFudge()); return Rcode.BADTIME; } return Rcode.NOERROR; } /** * Returns the maximum length of a TSIG record generated by this key. * * @see TSIGRecord */ public int recordLength() { return name.length() + 10 + alg.length() + 8 // time signed, fudge + 2 // 2 byte MAC length + algLengthMap.get(alg) + 4 // original id, error + 8; // 2 byte error length, 6 byte max error field. } private static void hmacAddSignature(Mac hmac, TSIGRecord tsig) { byte[] signatureSize = DNSOutput.toU16(tsig.getSignature().length); if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC signature size", signatureSize)); log.trace(hexdump.dump("TSIG-HMAC signature", tsig.getSignature())); } hmac.update(signatureSize); hmac.update(tsig.getSignature()); } private static void writeTsigTimerVariables(Instant instant, Duration fudge, DNSOutput out) { writeTsigTime(instant, out); out.writeU16((int) fudge.getSeconds()); } private static void writeTsigTime(Instant instant, DNSOutput out) { long time = instant.getEpochSecond(); int timeHigh = (int) (time >> 32); long timeLow = time & 0xFFFFFFFFL; out.writeU16(timeHigh); out.writeU32(timeLow); } /** * A utility class for generating signed message responses. * * @since 3.5.3 */ public static class StreamGenerator { private final TSIG key; private final Mac sharedHmac; private final int signEveryNthMessage; private int numGenerated; private TSIGRecord lastTsigRecord; /** * Creates an instance to sign multiple message for use in a stream. * *

This class creates a {@link TSIGRecord} on every message to conform with RFC 8945, 5.3.1. * * @param key The TSIG key used to create the signature records. * @param queryTsig The initial TSIG records, e.g. from a query to a server. */ public StreamGenerator(TSIG key, TSIGRecord queryTsig) { // The TSIG MUST be included on all DNS messages in the response. this(key, queryTsig, 1); } /** * This constructor is only for unit-testing {@link StreamVerifier} with responses where * not every message is signed. */ StreamGenerator(TSIG key, TSIGRecord queryTsig, int signEveryNthMessage) { if (signEveryNthMessage < 1 || signEveryNthMessage > 100) { throw new IllegalArgumentException("signEveryNthMessage must be between 1 and 100"); } this.key = key; this.lastTsigRecord = queryTsig; this.signEveryNthMessage = signEveryNthMessage; sharedHmac = this.key.initHmac(); } /** * Generate TSIG a signature for use of the message in a stream. * * @param message The message to sign. */ public void generate(Message message) { generate(message, true); } void generate(Message message, boolean isLastMessage) { boolean isNthMessage = numGenerated % signEveryNthMessage == 0; boolean isFirstMessage = numGenerated == 0; if (isFirstMessage || isNthMessage || isLastMessage) { TSIGRecord r = key.generate( message, message.toWire(), Rcode.NOERROR, isFirstMessage ? lastTsigRecord : null, isFirstMessage, sharedHmac); message.addRecord(r, Section.ADDITIONAL); message.tsigState = Message.TSIG_SIGNED; lastTsigRecord = r; hmacAddSignature(sharedHmac, r); } else { byte[] responseBytes = message.toWire(Message.MAXLENGTH); sharedHmac.update(responseBytes); } numGenerated++; } } /** A utility class for verifying multiple message responses. */ public static class StreamVerifier { private final TSIG key; private final Mac sharedHmac; private final TSIGRecord queryTsig; private int nresponses; private int lastsigned; /** {@code null} or the detailed error when validation failed due to a {@link Rcode#FORMERR}. */ @Getter private String errorMessage; /** Creates an object to verify a multiple message response */ public StreamVerifier(TSIG tsig, TSIGRecord queryTsig) { key = tsig; sharedHmac = key.initHmac(); nresponses = 0; this.queryTsig = queryTsig; } /** * Verifies a TSIG record on an incoming message that is part of a multiple message response. * TSIG records must be present on the first and last messages, and at least every 100 records * in between. After calling this routine,{@link Message#isVerified()} may be called on this * message. * *

This overload assumes that the verified message is not the last one, which is required to * have a {@link TSIGRecord}. Use {@link #verify(Message, byte[], boolean)} to explicitly * specify the last message or check that the message is verified with {@link * Message#isVerified()}. * * @param message The message * @param messageBytes The message in unparsed form * @return The result of the verification (as an Rcode) * @see Rcode */ public int verify(Message message, byte[] messageBytes) { return verify(message, messageBytes, false); } /** * Verifies a TSIG record on an incoming message that is part of a multiple message response. * TSIG records must be present on the first and last messages, and at least every 100 records * in between. After calling this routine, {@link Message#isVerified()} may be called on this * message. * * @param message The message * @param messageBytes The message in unparsed form * @param isLastMessage If true, verifies that the {@link Message} has an {@link TSIGRecord}. * @return The result of the verification (as an Rcode) * @see Rcode * @since 3.5.3 */ public int verify(Message message, byte[] messageBytes, boolean isLastMessage) { final String warningPrefix = "FORMERR: {}"; TSIGRecord tsig = message.getTSIG(); // https://datatracker.ietf.org/doc/html/rfc8945#section-5.3.1 // [...] a client that receives DNS messages and verifies TSIG MUST accept up to 99 // intermediary messages without a TSIG and MUST verify that both the first and last message // contain a TSIG. nresponses++; if (nresponses == 1) { if (tsig != null) { int result = key.verify(message, messageBytes, queryTsig, true, sharedHmac); hmacAddSignature(sharedHmac, tsig); lastsigned = nresponses; return result; } else { errorMessage = "missing required signature on first message"; log.debug(warningPrefix, errorMessage); message.tsigState = Message.TSIG_FAILED; return Rcode.FORMERR; } } if (tsig != null) { int result = key.verify(message, messageBytes, null, false, sharedHmac); lastsigned = nresponses; hmacAddSignature(sharedHmac, tsig); return result; } else { boolean required = nresponses - lastsigned >= 100; if (required) { errorMessage = "Missing required signature on message #" + nresponses; log.debug(warningPrefix, errorMessage); message.tsigState = Message.TSIG_FAILED; return Rcode.FORMERR; } else if (isLastMessage) { errorMessage = "Missing required signature on last message"; log.debug(warningPrefix, errorMessage); message.tsigState = Message.TSIG_FAILED; return Rcode.FORMERR; } else { errorMessage = "Intermediate message #" + nresponses + " without signature"; log.debug(warningPrefix, errorMessage); addUnsignedMessageToMac(message, messageBytes, sharedHmac); return Rcode.NOERROR; } } } private void addUnsignedMessageToMac(Message m, byte[] messageBytes, Mac hmac) { byte[] header = m.getHeader().toWire(); if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC header", header)); } hmac.update(header); int len = messageBytes.length - header.length; if (log.isTraceEnabled()) { log.trace(hexdump.dump("TSIG-HMAC message after header", messageBytes, header.length, len)); } hmac.update(messageBytes, header.length, len); m.tsigState = Message.TSIG_INTERMEDIATE; } } }