package model.x501; import annotations.Assoc; import model.asn1.*; import model.asn1.exceptions.ParseException; import model.asn1.parsing.BytesReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Represents an X.501 directory Name (a.k.a. RDNSequence). *
 *     Name ::= CHOICE { -- only one possibility for now -- rdnSequence RDNSequence }
 *     RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
 *     DistinguishedName ::= RDNSequence
 * 
*/ public class Name extends ASN1Object { @Assoc(partOf = true) private final RelativeDistinguishedName[] rdnSequence; /** * EFFECT: Initialize the Name with the given tags and rdnSequence. For tag and parentTag, consult * {@link ASN1Object}. * REQUIRES: Items should have SET tag. */ public Name(Tag tag, Tag parentTag, RelativeDistinguishedName[] rdnSequence) { super(tag, parentTag); this.rdnSequence = rdnSequence; } /** * EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}. * Throws {@link ParseException} for invalid input. * MODIFIES: this, encoded */ public Name(BytesReader encoded, boolean hasParentTag) throws ParseException { super(encoded, hasParentTag); final List list = new ArrayList<>(); for (int i = 0; i < getLength(); ) { int index = encoded.getIndex(); final RelativeDistinguishedName name = new RelativeDistinguishedName(encoded, false); name.getTag().enforce(TAG_SET); list.add(name); index = encoded.getIndex() - index; i += index; } this.rdnSequence = list.toArray(new RelativeDistinguishedName[0]); } /** * EFFECTS: Parse OID after last KV and clear context if input is '='. Otherwise add to context. * Throws {@link ParseException} if input is '+' or ',', or if the oid cannot be recognized. * MODIFIES: context */ private static ObjectIdentifier handleKey(char c, List context) throws ParseException { if (c == '=') { if (context.isEmpty()) { throw new ParseException("Unterminated key"); } final ObjectIdentifier oid = new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.getKnown( context.stream().map(Object::toString).collect(Collectors.joining("")))); context.clear(); return oid; } else if (c == '+' || c == ',') { throw new ParseException("Unterminated key part: " + context); } else { context.add(c); return null; } } /** * EFFECTS: Parse KV after '='. Clear context. * Throws {@link ParseException} if context is empty. * MODIFIES: context * REQUIRES: curKey to be a valid OID */ private static AttributeTypeAndValue flushKV(ObjectIdentifier curKey, List context) throws ParseException { if (context.isEmpty()) { throw new ParseException("Unterminated value"); } final AttributeTypeAndValue tv = new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, curKey, new PrintableString(PrintableString.TAG, null, context.stream().map(Object::toString).collect(Collectors.joining("")))); context.clear(); return tv; } /** * EFFECTS: Handle value after =, optionally flush to rdns after ',', or add to curListKT if after '+'. Clears * context if flushed, otherwise add to context. Returns whether switch to the state of reading key. * Throws {@link ParseException} if c is '=' or context is empty. * MODIFIES: context, curListKT, rdns * REQUIRES: curKey to be a valid OID */ private static boolean handleValue(char c, List context, List curListKT, ObjectIdentifier curKey, List rdns) throws ParseException { if (c == ',') { if (context.isEmpty()) { throw new ParseException("Unterminated value"); } curListKT.add(flushKV(curKey, context)); rdns.add(new RelativeDistinguishedName(ASN1Object.TAG_SET, null, curListKT.toArray(AttributeTypeAndValue[]::new))); curListKT.clear(); return true; } else if (c == '+') { curListKT.add(flushKV(curKey, context)); return true; } else if (c == '=') { throw new ParseException("Unterminated value part: " + context); } else { context.add(c); return false; } } /** * EFFECTS: Parse the given DN string into structural X.509 RDN Sequence. * Character literals = + , must be escaped. * Values will always be PrintableString. * Throws {@link ParseException} if invalid. */ public static Name parseString(String dn) throws ParseException { char state = 0; // 0 - Key, 1 - Value; MSB: Escaped List rdns = new ArrayList<>(); List curListKT = new ArrayList<>(); ObjectIdentifier curKey = null; List context = new ArrayList<>(); for (char c : (dn + ",").toCharArray()) { if ((state >> 7) == 1) { context.add(c); state &= 127; continue; } else if (c == '\\') { state |= 128; continue; } if (state == 0) { if ((curKey = handleKey(c, context)) != null) { state = 1; } } else if (handleValue(c, context, curListKT, curKey, rdns)) { state = 0; } } return new Name(ASN1Object.TAG_SEQUENCE, null, rdns.toArray(RelativeDistinguishedName[]::new)); } /** * EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one. */ @Override public Byte[] encodeValueDER() { return Stream.of(rdnSequence) .map(Encodable::encodeDER) .flatMap(Arrays::stream) .toArray(Byte[]::new); } /** * EFFECT: Convert the name into directory string, like CN=yuuta,OU=users,DC=yuuta,DC=moe */ @Override public String toString() { return Stream.of(rdnSequence) .map(RelativeDistinguishedName::toString) .collect(Collectors.joining(",")); } public RelativeDistinguishedName[] getRdnSequence() { return rdnSequence; } }