package model.asn1; import annotations.Assoc; import model.asn1.exceptions.ParseException; import model.asn1.parsing.BytesReader; /** * Represents the metadata (tag) of an ASN.1 type. */ public class Tag implements Encodable { @Assoc(partOf = true) private final TagClass cls; private final boolean constructive; private final int number; /** * EFFECTS: Construct the ASN.1 tag with the given class, constructive / primitive, and number. * REQUIRES: number > 0 (X.680$8.2) and number <= 31 (no high tag number support currently); * the constructive or primitive value must follow the X.680 spec, and it must be primitive if it * can be both constructive or primitive (according to X.690 DER). */ public Tag(TagClass cls, boolean constructive, int number) { this.cls = cls; this.constructive = constructive; this.number = number; } /** * EFFECTS: Initialize the tag by parsing class / constructive / number from the encoded DER bytes. * {@link ParseException} is thrown if the input is invalid: * - The encoded array must have at least one byte. * - The tag number is zero if the class is UNIVERSAL. * REQUIRES: The highest two bits must contain the class, and then the constructive bit, and finally the low 5 bits * must contain the tag number <= 31. * MODIFIES: encoded (one byte read) */ public Tag(BytesReader encoded) throws ParseException { final Byte val = encoded.require(1, true)[0]; // Take the highest two bits // -64 = 2's complement of 0b11000000 final int highestTwo = val & -64; if (highestTwo == TagClass.UNIVERSAL.getVal()) { this.cls = TagClass.UNIVERSAL; } else if (highestTwo == TagClass.APPLICATION.getVal()) { this.cls = TagClass.APPLICATION; } else if (highestTwo == TagClass.PRIVATE.getVal()) { this.cls = TagClass.PRIVATE; } else { this.cls = TagClass.CONTEXT_SPECIFIC; } // Detect the constructive bit by only keeping the corresponding bit and shifting it to the lowestbit. this.constructive = (val & 0x20) >> 5 == 1; // Parse the number by clearing the high 3 bits. this.number = val & 0x1F; if (this.cls == TagClass.UNIVERSAL && this.number == 0) { throw new ParseException(String.format("The tag number must not be zero for UNIVERSAL tags" + "(byte 0x%02X @ %d)", val, encoded.getIndex())); } } /** * EFFECTS: Encode that tag as DER bytes, as follows: * HI 7 6 | 5 | 4 3 2 1 0 LO * Class | C/P | Tag Number * Notes, In the domain of this application (PKI), a single byte is always returned * (as nothing requires high tag number). However, the return type is held as byte[] * to 1) compliant with the spec, 2) reserve for future scalability. */ @Override public Byte[] encodeDER() { // Fill the low 5bits with tag number byte value = (byte) number; // Fill the bit in-between with constructive bit if (constructive) { value |= 0x20; // 0b00100000: Enable the 5th bit } else { value &= 0xDF; // 0b11011111: Disable the 5th bit } // Fill the high two bits with tag class value |= cls.getVal(); return new Byte[]{value}; } /** * EFFECTS: Throw {@link ParseException} if this tag is not exactly the given tag. Useful for parsing. */ public void enforce(Tag tag) throws ParseException { if (this.number != tag.number || this.constructive != tag.constructive || this.cls != tag.cls) { throw new ParseException(String.format("Illegal tag: Expected %s[%d] but got %s[%d].", tag.cls, tag.number, cls, number)); } } public TagClass getCls() { return cls; } public boolean isConstructive() { return constructive; } public int getNumber() { return number; } }