blob: 1c340216b8d650f3478aa9a4f551b01775a6ba45 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.felix.framework.security.verifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public final class SubjectDNParser
{
private static final Map OID2NAME = new HashMap();
static
{
// see core-spec 2.3.5
OID2NAME.put("2.5.4.3", "cn");
OID2NAME.put("2.5.4.4", "sn");
OID2NAME.put("2.5.4.6", "c");
OID2NAME.put("2.5.4.7", "l");
OID2NAME.put("2.5.4.8", "st");
OID2NAME.put("2.5.4.10", "o");
OID2NAME.put("2.5.4.11", "ou");
OID2NAME.put("2.5.4.12", "title");
OID2NAME.put("2.5.4.42", "givenname");
OID2NAME.put("2.5.4.43", "initials");
OID2NAME.put("2.5.4.44", "generationqualifier");
OID2NAME.put("2.5.4.46", "dnqualifier");
OID2NAME.put("2.5.4.9", "street");
OID2NAME.put("0.9.2342.19200300.100.1.25", "dc");
OID2NAME.put("0.9.2342.19200300.100.1.1", "uid");
OID2NAME.put("1.2.840.113549.1.9.1", "emailaddress");
OID2NAME.put("2.5.4.5", "serialnumber");
// p.s.: it sucks that the spec doesn't list some of the oids used
// p.p.s: it sucks that the spec doesn't list the short form for all names
// In summary, there is a certain amount of guess-work involved but I'm
// fairly certain I've got it right.
}
private byte[] m_buffer;
private int m_offset = 0;
private int m_tagOffset = 0;
private int m_tag = -1;
private int m_length = -1;
private int m_contentOffset = -1;
/*
* This is deep magiK, bare with me. The problem is that we don't get
* access to the original subject dn in a certificate without resorting to
* sun.* classes or running on something > OSGi-minimum/jdk1.3. Furthermore,
* we need access to it because there is no other way to escape it properly.
* Note, this is due to missing of a public X500Name in OSGI-minimum/jdk1.3
* a.k.a foundation.
*
* The solution is to get the DER encoded TBS certificate bytes via the
* available java methods and parse-out the subject dn in canonical form by
* hand. This is possible without deploying a full-blown BER encoder/decoder
* due to java already having done all the cumbersome verification and
* normalization work.
*
* The following skips through the TBS certificate bytes until it reaches and
* subsequently parses the subject dn. If the below makes immediate sense to
* you - you either are a X509/X501/DER expert or quite possibly mad. In any
* case, please seek medical care immediately.
*/
public String parseSubjectDN(byte[] tbsBuffer) throws Exception
{
// init
m_buffer = tbsBuffer;
m_offset = 0;
// TBSCertificate ::= SEQUENCE {
// version [0] EXPLICIT Version DEFAULT v1,
// serialNumber CertificateSerialNumber,
// signature AlgorithmIdentifier,
// issuer Name,
// validity Validity,
// subject Name,
//
// WE CAN STOP!
//
// subjectPublicKeyInfo SubjectPublicKeyInfo,
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
// -- If present, version must be v2 or v3
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
// -- If present, version must be v2 or v3
// extensions [3] EXPLICIT Extensions OPTIONAL
// -- If present, version must be v3
// }
try
{
next();
next();
// if a version is present skip it
if (m_tag == 0)
{
next();
m_offset += m_length;
}
m_offset += m_length;
// skip the serialNumber
next();
next();
m_offset += m_length;
// skip the signature
next();
m_offset += m_length;
// skip the issuer
// The issuer is a sequence of sets of issuer dns like the subject later on -
// we just skip it.
next();
int endOffset = m_offset + m_length;
int seqTagOffset = m_tagOffset;
// skip the sequence
while (endOffset > m_offset)
{
next();
int endOffset2 = m_offset + m_length;
int seqTagOffset2 = m_tagOffset;
// skip each set
while (endOffset2 > m_offset)
{
next();
next();
m_offset += m_length;
next();
m_offset += m_length;
}
m_tagOffset = seqTagOffset2;
}
m_tagOffset = seqTagOffset;
// skip the validity which contains two dates to be skiped
next();
next();
m_offset += m_length;
next();
m_offset += m_length;
next();
// Now extract the subject dns and add them to attributes
List attributes = new ArrayList();
endOffset = m_offset + m_length;
seqTagOffset = m_tagOffset;
// for each set of rdns
while (endOffset > m_offset)
{
next();
int endOffset2 = m_offset + m_length;
// store tag offset
int seqTagOffset2 = m_tagOffset;
List rdn = new ArrayList();
// for each rdn in the set
while (endOffset2 > m_offset)
{
next();
next();
m_offset += m_length;
// parse the oid of the rdn
int oidElement = 1;
for (int i = 0; i < m_length; i++, ++oidElement)
{
while ((m_buffer[m_contentOffset + i] & 0x80) == 0x80)
{
i++;
}
}
int[] oid = new int[oidElement];
for (int id = 1, i = 0; id < oid.length; id++, i++)
{
int octet = m_buffer[m_contentOffset + i];
oidElement = octet & 0x7F;
while ((octet & 0x80) != 0)
{
i++;
octet = m_buffer[m_contentOffset + i];
oidElement = oidElement << 7 | (octet & 0x7f);
}
oid[id] = oidElement;
}
// The first OID is special
if (oid[1] > 79)
{
oid[0] = 2;
oid[1] = oid[1] - 80;
}
else
{
oid[0] = oid[1] / 40;
oid[1] = oid[1] % 40;
}
// Now parse the value of the rdn
next();
String str = null;
int tagTmp = m_tag;
m_offset += m_length;
switch(tagTmp)
{
case 30: // BMPSTRING
case 22: // IA5STRING
case 27: // GENERALSTRING
case 19: // PRINTABLESTRING
case 20: // TELETEXSTRING && T61STRING
case 28: // UNIVERSALSTRING
str = new String(m_buffer, m_contentOffset,
m_length);
break;
case 12: // UTF8_STRING
str = new String(m_buffer, m_contentOffset,
m_length, "UTF-8");
break;
default: // OCTET
byte[] encoded = new byte[m_offset - m_tagOffset];
System.arraycopy(m_buffer, m_tagOffset, encoded,
0, encoded.length);
// Note, I'm not sure this is allowed by the spec
// i.e., whether OCTET subjects are allowed at all
// but it shouldn't harm doing it anyways (we just
// convert it into a hex string prefixed with \#).
str = toHexString(encoded);
break;
}
rdn.add(new Object[]{mapOID(oid), makeCanonical(str)});
}
attributes.add(rdn);
m_tagOffset = seqTagOffset2;
}
m_tagOffset = seqTagOffset;
StringBuffer result = new StringBuffer();
for (int i = attributes.size() - 1; i >= 0; i--)
{
List rdn = (List) attributes.get(i);
Collections.sort(rdn, new Comparator()
{
public int compare(Object obj1, Object obj2)
{
return ((String) ((Object[]) obj1)[0]).compareTo(
((String) ((Object[])obj2)[0]));
}
});
for (Iterator iter = rdn.iterator();iter.hasNext();)
{
Object[] att = (Object[]) iter.next();
result.append((String) att[0]);
result.append('=');
result.append((String) att[1]);
if (iter.hasNext())
{
// multi-valued RDN
result.append('+');
}
}
if (i != 0)
{
result.append(',');
}
}
// the spec says:
// return result.toString().toUpperCase(Locale.US).toLowerCase(Locale.US);
// which is needed because toLowerCase can be ambiguous in unicode when
// used on mixed case while toUpperCase not hence, this way its ok.
return result.toString().toUpperCase(Locale.US).toLowerCase(Locale.US);
}
finally
{
m_buffer = null;
}
}
// Determine the type of the current sequence (tbs_tab), and the length and
// offset of it (tbs_length and tbs_tagOffset) plus increment the global
// offset (tbs_offset) accordingly. Note, we don't need to check for
// the indefinite length because this is supposed to be DER not BER (and
// we implicitly assume that java only gives us valid DER).
private void next()
{
m_tagOffset = m_offset;
m_tag = m_buffer[m_offset++] & 0xFF;
m_length = m_buffer[m_offset++] & 0xFF;
// There are two kinds of length forms - make sure we use the right one
if ((m_length & 0x80) != 0)
{
// its the long kind
int numOctets = m_length & 0x7F;
// hence, convert it
m_length = m_buffer[m_offset++] & 0xFF;
for (int i = 1; i < numOctets; i++)
{
int ch = m_buffer[m_offset++] & 0xFF;
m_length = (m_length << 8) + ch;
}
}
m_contentOffset = m_offset;
}
private String makeCanonical(String value)
{
int len = value.length();
if (len == 0)
{
return value;
}
StringBuffer result = new StringBuffer(len);
int i = 0;
if (value.charAt(0) == '#')
{
result.append('\\');
result.append('#');
i++;
}
for (;i < len; i++)
{
char c = value.charAt(i);
switch (c)
{
case ' ':
int pos = result.length();
// remove leading spaces and
// remove all spaces except one in any sequence of spaces
if ((pos == 0) || (result.charAt(pos - 1) == ' '))
{
break;
}
result.append(' ');
break;
case '"':
case '\\':
case ',':
case '+':
case '<':
case '>':
case ';':
result.append('\\');
default:
result.append(c);
}
}
// count down until first none space to remove trailing spaces
i = result.length() - 1;
while ((i > -1) && (result.charAt(i) == ' '))
{
i--;
}
result.setLength(i + 1);
return result.toString();
}
private String toHexString(byte[] encoded)
{
StringBuffer result = new StringBuffer();
result.append('#');
for (int i = 0; i < encoded.length; i++)
{
int c = (encoded[i] >> 4) & 0x0F;
if (c < 10)
{
result.append((char) (c + 48));
}
else
{
result.append((char) (c + 87));
}
c = encoded[i] & 0x0F;
if (c < 10)
{
result.append((char) (c + 48));
}
else
{
result.append((char) (c + 87));
}
}
return result.toString();
}
// This just creates a string of the oid and looks for its name in the
// known names map OID2NAME. There might be faster implementations :-)
private String mapOID(int[] oid)
{
StringBuffer oidString = new StringBuffer();
oidString.append(oid[0]);
for (int i = 1;i < oid.length;i++)
{
oidString.append('.');
oidString.append(oid[i]);
}
String result = (String) OID2NAME.get(oidString.toString());
if (result == null)
{
throw new IllegalArgumentException("Unknown oid: " + oidString.toString());
}
return result;
}
}