| // Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) |
| |
| package org.xbill.DNS; |
| |
| import java.util.*; |
| import org.xbill.DNS.utils.*; |
| |
| /** |
| * 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 TSIG key; |
| private 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; |
| } |
| } |
| |
| } |