/* * Copied from the DnsJava project * * Copyright (c) 1998-2011, Brian Wellington. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package io.milton.dns.record; import io.milton.dns.Name; import io.milton.dns.TextParseException; import io.milton.dns.utils.HMAC; import io.milton.dns.utils.base64; import java.util.*; /** * Transaction signature handling. This class generates and verifies * TSIG records on messages, which provide transaction security. * @see TSIGRecord * * @author Brian Wellington */ public class TSIG { private static final String HMAC_MD5_STR = "HMAC-MD5.SIG-ALG.REG.INT."; private static final String HMAC_SHA1_STR = "hmac-sha1."; private static final String HMAC_SHA224_STR = "hmac-sha224."; private static final String HMAC_SHA256_STR = "hmac-sha256."; private static final String HMAC_SHA384_STR = "hmac-sha384."; private static final String HMAC_SHA512_STR = "hmac-sha512."; /** The domain name representing the HMAC-MD5 algorithm. */ public static final Name HMAC_MD5 = Name.fromConstantString(HMAC_MD5_STR); /** The domain name representing the HMAC-MD5 algorithm (deprecated). */ public static final Name HMAC = HMAC_MD5; /** The domain name representing the HMAC-SHA1 algorithm. */ public static final Name HMAC_SHA1 = Name.fromConstantString(HMAC_SHA1_STR); /** * The domain name representing the HMAC-SHA224 algorithm. * Note that SHA224 is not supported by Java out-of-the-box, this requires use * of a third party provider like BouncyCastle.org. */ public static final Name HMAC_SHA224 = Name.fromConstantString(HMAC_SHA224_STR); /** The domain name representing the HMAC-SHA256 algorithm. */ public static final Name HMAC_SHA256 = Name.fromConstantString(HMAC_SHA256_STR); /** The domain name representing the HMAC-SHA384 algorithm. */ public static final Name HMAC_SHA384 = Name.fromConstantString(HMAC_SHA384_STR); /** The domain name representing the HMAC-SHA512 algorithm. */ public static final Name HMAC_SHA512 = Name.fromConstantString(HMAC_SHA512_STR); /** * The default fudge value for outgoing packets. Can be overriden by the * tsigfudge option. */ public static final short FUDGE = 300; private Name name, alg; private String digest; private int digestBlockLength; private byte [] key; private void getDigest() { if (alg.equals(HMAC_MD5)) { digest = "md5"; digestBlockLength = 64; } else if (alg.equals(HMAC_SHA1)) { digest = "sha-1"; digestBlockLength = 64; } else if (alg.equals(HMAC_SHA224)) { digest = "sha-224"; digestBlockLength = 64; } else if (alg.equals(HMAC_SHA256)) { digest = "sha-256"; digestBlockLength = 64; } else if (alg.equals(HMAC_SHA512)) { digest = "sha-512"; digestBlockLength = 128; } else if (alg.equals(HMAC_SHA384)) { digest = "sha-384"; digestBlockLength = 128; } else throw new IllegalArgumentException("Invalid 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's data. */ public TSIG(Name algorithm, Name name, byte [] key) { this.name = name; this.alg = algorithm; this.key = key; getDigest(); } /** * Creates a new TSIG key with the 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. */ 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) { this.key = base64.fromString(key); if (this.key == 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; getDigest(); } /** * Creates a new TSIG object, which can be used to sign or verify a message. * @param name The name of the shared key. * @param algorithm The algorithm of the shared key. The legal values are * "hmac-md5", "hmac-sha1", "hmac-sha224", "hmac-sha256", "hmac-sha384", and * "hmac-sha512". * @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(String algorithm, String name, String key) { this(HMAC_MD5, name, key); if (algorithm.equalsIgnoreCase("hmac-md5")) this.alg = HMAC_MD5; else if (algorithm.equalsIgnoreCase("hmac-sha1")) this.alg = HMAC_SHA1; else if (algorithm.equalsIgnoreCase("hmac-sha224")) this.alg = HMAC_SHA224; else if (algorithm.equalsIgnoreCase("hmac-sha256")) this.alg = HMAC_SHA256; else if (algorithm.equalsIgnoreCase("hmac-sha384")) this.alg = HMAC_SHA384; else if (algorithm.equalsIgnoreCase("hmac-sha512")) this.alg = HMAC_SHA512; else throw new IllegalArgumentException("Invalid TSIG algorithm"); getDigest(); } /** * Creates a new TSIG object with the 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 */ 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 an algorithm is specified, it must * be "hmac-md5", "hmac-sha1", or "hmac-sha256". * @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 */ static public TSIG fromString(String str) { String [] parts = str.split("[:/]", 3); if (parts.length < 2) throw new IllegalArgumentException("Invalid TSIG key " + "specification"); if (parts.length == 3) { try { return new TSIG(parts[0], parts[1], parts[2]); } catch (IllegalArgumentException e) { parts = str.split("[:/]", 2); } } return new TSIG(HMAC_MD5, parts[0], parts[1]); } /** * 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) { Date timeSigned; if (error != Rcode.BADTIME) timeSigned = new Date(); else timeSigned = old.getTimeSigned(); int fudge; HMAC hmac = null; if (error == Rcode.NOERROR || error == Rcode.BADTIME) hmac = new HMAC(digest, digestBlockLength, key); fudge = Options.intValue("tsigfudge"); if (fudge < 0 || fudge > 0x7FFF) fudge = FUDGE; if (old != null) { DNSOutput out = new DNSOutput(); out.writeU16(old.getSignature().length); if (hmac != null) { hmac.update(out.toByteArray()); hmac.update(old.getSignature()); } } /* Digest the message */ if (hmac != null) hmac.update(b); DNSOutput out = new DNSOutput(); name.toWireCanonical(out); out.writeU16(DClass.ANY); /* class */ out.writeU32(0); /* ttl */ alg.toWireCanonical(out); long time = timeSigned.getTime() / 1000; int timeHigh = (int) (time >> 32); long timeLow = (time & 0xFFFFFFFFL); out.writeU16(timeHigh); out.writeU32(timeLow); out.writeU16(fudge); out.writeU16(error); out.writeU16(0); /* No other data */ if (hmac != null) hmac.update(out.toByteArray()); byte [] signature; if (hmac != null) signature = hmac.sign(); else signature = new byte[0]; byte [] other = null; if (error == Rcode.BADTIME) { out = new DNSOutput(); time = new Date().getTime() / 1000; timeHigh = (int) (time >> 32); timeLow = (time & 0xFFFFFFFFL); out.writeU16(timeHigh); out.writeU32(timeLow); other = out.toByteArray(); } return (new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge, signature, m.getHeader().getID(), error, other)); } /** * 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) { Record r = generate(m, m.toWire(), error, old); 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 */ public void apply(Message m, TSIGRecord old) { apply(m, Rcode.NOERROR, old); } /** * 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 applyStream(Message m, TSIGRecord old, boolean first) { if (first) { apply(m, old); return; } Date timeSigned = new Date(); int fudge; HMAC hmac = new HMAC(digest, digestBlockLength, key); fudge = Options.intValue("tsigfudge"); if (fudge < 0 || fudge > 0x7FFF) fudge = FUDGE; DNSOutput out = new DNSOutput(); out.writeU16(old.getSignature().length); hmac.update(out.toByteArray()); hmac.update(old.getSignature()); /* Digest the message */ hmac.update(m.toWire()); out = new DNSOutput(); long time = timeSigned.getTime() / 1000; int timeHigh = (int) (time >> 32); long timeLow = (time & 0xFFFFFFFFL); out.writeU16(timeHigh); out.writeU32(timeLow); out.writeU16(fudge); hmac.update(out.toByteArray()); byte [] signature = hmac.sign(); byte [] other = null; Record r = new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge, signature, m.getHeader().getID(), Rcode.NOERROR, other); m.addRecord(r, Section.ADDITIONAL); m.tsigState = Message.TSIG_SIGNED; } /** * 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 * @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 The length of the message in the array. * @param old If this message is a response, the TSIG from the request * @return The result of the verification (as an Rcode) * @see Rcode */ public byte verify(Message m, byte [] b, int length, TSIGRecord old) { m.tsigState = Message.TSIG_FAILED; TSIGRecord tsig = m.getTSIG(); HMAC hmac = new HMAC(digest, digestBlockLength, key); if (tsig == null) return Rcode.FORMERR; if (!tsig.getName().equals(name) || !tsig.getAlgorithm().equals(alg)) { if (Options.check("verbose")) System.err.println("BADKEY failure"); return Rcode.BADKEY; } long now = System.currentTimeMillis(); long then = tsig.getTimeSigned().getTime(); long fudge = tsig.getFudge(); if (Math.abs(now - then) > fudge * 1000) { if (Options.check("verbose")) System.err.println("BADTIME failure"); return Rcode.BADTIME; } if (old != null && tsig.getError() != Rcode.BADKEY && tsig.getError() != Rcode.BADSIG) { DNSOutput out = new DNSOutput(); out.writeU16(old.getSignature().length); hmac.update(out.toByteArray()); hmac.update(old.getSignature()); } m.getHeader().decCount(Section.ADDITIONAL); byte [] header = m.getHeader().toWire(); m.getHeader().incCount(Section.ADDITIONAL); hmac.update(header); int len = m.tsigstart - header.length; hmac.update(b, header.length, len); DNSOutput out = new DNSOutput(); tsig.getName().toWireCanonical(out); out.writeU16(tsig.dclass); out.writeU32(tsig.ttl); tsig.getAlgorithm().toWireCanonical(out); long time = tsig.getTimeSigned().getTime() / 1000; int timeHigh = (int) (time >> 32); long timeLow = (time & 0xFFFFFFFFL); out.writeU16(timeHigh); out.writeU32(timeLow); out.writeU16(tsig.getFudge()); out.writeU16(tsig.getError()); if (tsig.getOther() != null) { out.writeU16(tsig.getOther().length); out.writeByteArray(tsig.getOther()); } else { out.writeU16(0); } hmac.update(out.toByteArray()); byte [] signature = tsig.getSignature(); int digestLength = hmac.digestLength(); int minDigestLength = digest.equals("md5") ? 10 : digestLength / 2; if (signature.length > digestLength) { if (Options.check("verbose")) System.err.println("BADSIG: signature too long"); return Rcode.BADSIG; } else if (signature.length < minDigestLength) { if (Options.check("verbose")) System.err.println("BADSIG: signature too short"); return Rcode.BADSIG; } else if (!hmac.verify(signature, true)) { if (Options.check("verbose")) System.err.println("BADSIG: signature verification"); return Rcode.BADSIG; } m.tsigState = Message.TSIG_VERIFIED; return Rcode.NOERROR; } /** * 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 * @param b 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 old 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 [] b, TSIGRecord old) { return verify(m, b, b.length, old); } /** * 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 18 + // 2 byte MAC length, 16 byte MAC 4 + // original id, error 8); // 2 byte error length, 6 byte max error field. } public static class StreamVerifier { /** * A helper class for verifying multiple message responses. */ private final TSIG key; private final HMAC verifier; private int nresponses; private int lastsigned; private TSIGRecord lastTSIG; /** Creates an object to verify a multiple message response */ public StreamVerifier(TSIG tsig, TSIGRecord old) { key = tsig; verifier = new HMAC(key.digest, key.digestBlockLength, key.key); nresponses = 0; lastTSIG = old; } /** * 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, Message.isVerified() may be called on * this message. * @param m The message * @param b The message in unparsed form * @return The result of the verification (as an Rcode) * @see Rcode */ public int verify(Message m, byte [] b) { TSIGRecord tsig = m.getTSIG(); nresponses++; if (nresponses == 1) { int result = key.verify(m, b, lastTSIG); if (result == Rcode.NOERROR) { byte [] signature = tsig.getSignature(); DNSOutput out = new DNSOutput(); out.writeU16(signature.length); verifier.update(out.toByteArray()); verifier.update(signature); } lastTSIG = tsig; return result; } if (tsig != null) m.getHeader().decCount(Section.ADDITIONAL); byte [] header = m.getHeader().toWire(); if (tsig != null) m.getHeader().incCount(Section.ADDITIONAL); verifier.update(header); int len; if (tsig == null) len = b.length - header.length; else len = m.tsigstart - header.length; verifier.update(b, header.length, len); if (tsig != null) { lastsigned = nresponses; lastTSIG = tsig; } else { boolean required = (nresponses - lastsigned >= 100); if (required) { m.tsigState = Message.TSIG_FAILED; return Rcode.FORMERR; } else { m.tsigState = Message.TSIG_INTERMEDIATE; return Rcode.NOERROR; } } if (!tsig.getName().equals(key.name) || !tsig.getAlgorithm().equals(key.alg)) { if (Options.check("verbose")) System.err.println("BADKEY failure"); m.tsigState = Message.TSIG_FAILED; return Rcode.BADKEY; } DNSOutput out = new DNSOutput(); long time = tsig.getTimeSigned().getTime() / 1000; int timeHigh = (int) (time >> 32); long timeLow = (time & 0xFFFFFFFFL); out.writeU16(timeHigh); out.writeU32(timeLow); out.writeU16(tsig.getFudge()); verifier.update(out.toByteArray()); if (verifier.verify(tsig.getSignature()) == false) { if (Options.check("verbose")) System.err.println("BADSIG failure"); m.tsigState = Message.TSIG_FAILED; return Rcode.BADSIG; } verifier.clear(); out = new DNSOutput(); out.writeU16(tsig.getSignature().length); verifier.update(out.toByteArray()); verifier.update(tsig.getSignature()); m.tsigState = Message.TSIG_VERIFIED; return Rcode.NOERROR; } } }