diff options
Diffstat (limited to 'src/org/xbill/DNS/TSIG.java')
-rw-r--r-- | src/org/xbill/DNS/TSIG.java | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/src/org/xbill/DNS/TSIG.java b/src/org/xbill/DNS/TSIG.java new file mode 100644 index 0000000..d9e6972 --- /dev/null +++ b/src/org/xbill/DNS/TSIG.java @@ -0,0 +1,592 @@ +// 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; + } +} + +} |