394 lines
11 KiB
C#
394 lines
11 KiB
C#
//
|
|
// X501Name.cs: X.501 Distinguished Names stuff
|
|
//
|
|
// Author:
|
|
// Sebastien Pouliot <sebastien@ximian.com>
|
|
//
|
|
// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
|
|
// Copyright (C) 2004-2006 Novell, Inc (http://www.novell.com)
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
// the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace Emby.Server.Core.Cryptography
|
|
{
|
|
|
|
// References:
|
|
// 1. Information technology - Open Systems Interconnection - The Directory: Models
|
|
// http://www.itu.int/rec/recommendation.asp?type=items&lang=e&parent=T-REC-X.501-200102-I
|
|
// 2. RFC2253: Lightweight Directory Access Protocol (v3): UTF-8 String Representation of Distinguished Names
|
|
// http://www.ietf.org/rfc/rfc2253.txt
|
|
|
|
/*
|
|
* Name ::= CHOICE { RDNSequence }
|
|
*
|
|
* RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
|
|
*
|
|
* RelativeDistinguishedName ::= SET OF AttributeTypeAndValue
|
|
*/
|
|
public sealed class X501 {
|
|
|
|
static byte[] countryName = { 0x55, 0x04, 0x06 };
|
|
static byte[] organizationName = { 0x55, 0x04, 0x0A };
|
|
static byte[] organizationalUnitName = { 0x55, 0x04, 0x0B };
|
|
static byte[] commonName = { 0x55, 0x04, 0x03 };
|
|
static byte[] localityName = { 0x55, 0x04, 0x07 };
|
|
static byte[] stateOrProvinceName = { 0x55, 0x04, 0x08 };
|
|
static byte[] streetAddress = { 0x55, 0x04, 0x09 };
|
|
//static byte[] serialNumber = { 0x55, 0x04, 0x05 };
|
|
static byte[] domainComponent = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x19 };
|
|
static byte[] userid = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x01 };
|
|
static byte[] email = { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 };
|
|
static byte[] dnQualifier = { 0x55, 0x04, 0x2E };
|
|
static byte[] title = { 0x55, 0x04, 0x0C };
|
|
static byte[] surname = { 0x55, 0x04, 0x04 };
|
|
static byte[] givenName = { 0x55, 0x04, 0x2A };
|
|
static byte[] initial = { 0x55, 0x04, 0x2B };
|
|
|
|
private X501 ()
|
|
{
|
|
}
|
|
|
|
static public string ToString (ASN1 seq)
|
|
{
|
|
StringBuilder sb = new StringBuilder ();
|
|
for (int i = 0; i < seq.Count; i++) {
|
|
ASN1 entry = seq [i];
|
|
AppendEntry (sb, entry, true);
|
|
|
|
// separator (not on last iteration)
|
|
if (i < seq.Count - 1)
|
|
sb.Append (", ");
|
|
}
|
|
return sb.ToString ();
|
|
}
|
|
|
|
static public string ToString (ASN1 seq, bool reversed, string separator, bool quotes)
|
|
{
|
|
StringBuilder sb = new StringBuilder ();
|
|
|
|
if (reversed) {
|
|
for (int i = seq.Count - 1; i >= 0; i--) {
|
|
ASN1 entry = seq [i];
|
|
AppendEntry (sb, entry, quotes);
|
|
|
|
// separator (not on last iteration)
|
|
if (i > 0)
|
|
sb.Append (separator);
|
|
}
|
|
} else {
|
|
for (int i = 0; i < seq.Count; i++) {
|
|
ASN1 entry = seq [i];
|
|
AppendEntry (sb, entry, quotes);
|
|
|
|
// separator (not on last iteration)
|
|
if (i < seq.Count - 1)
|
|
sb.Append (separator);
|
|
}
|
|
}
|
|
return sb.ToString ();
|
|
}
|
|
|
|
static private void AppendEntry (StringBuilder sb, ASN1 entry, bool quotes)
|
|
{
|
|
// multiple entries are valid
|
|
for (int k = 0; k < entry.Count; k++) {
|
|
ASN1 pair = entry [k];
|
|
ASN1 s = pair [1];
|
|
if (s == null)
|
|
continue;
|
|
|
|
ASN1 poid = pair [0];
|
|
if (poid == null)
|
|
continue;
|
|
|
|
if (poid.CompareValue (countryName))
|
|
sb.Append ("C=");
|
|
else if (poid.CompareValue (organizationName))
|
|
sb.Append ("O=");
|
|
else if (poid.CompareValue (organizationalUnitName))
|
|
sb.Append ("OU=");
|
|
else if (poid.CompareValue (commonName))
|
|
sb.Append ("CN=");
|
|
else if (poid.CompareValue (localityName))
|
|
sb.Append ("L=");
|
|
else if (poid.CompareValue (stateOrProvinceName))
|
|
sb.Append ("S="); // NOTE: RFC2253 uses ST=
|
|
else if (poid.CompareValue (streetAddress))
|
|
sb.Append ("STREET=");
|
|
else if (poid.CompareValue (domainComponent))
|
|
sb.Append ("DC=");
|
|
else if (poid.CompareValue (userid))
|
|
sb.Append ("UID=");
|
|
else if (poid.CompareValue (email))
|
|
sb.Append ("E="); // NOTE: Not part of RFC2253
|
|
else if (poid.CompareValue (dnQualifier))
|
|
sb.Append ("dnQualifier=");
|
|
else if (poid.CompareValue (title))
|
|
sb.Append ("T=");
|
|
else if (poid.CompareValue (surname))
|
|
sb.Append ("SN=");
|
|
else if (poid.CompareValue (givenName))
|
|
sb.Append ("G=");
|
|
else if (poid.CompareValue (initial))
|
|
sb.Append ("I=");
|
|
else {
|
|
// unknown OID
|
|
sb.Append ("OID."); // NOTE: Not present as RFC2253
|
|
sb.Append (ASN1Convert.ToOid (poid));
|
|
sb.Append ("=");
|
|
}
|
|
|
|
string sValue = null;
|
|
// 16bits or 8bits string ? TODO not complete (+special chars!)
|
|
if (s.Tag == 0x1E) {
|
|
// BMPSTRING
|
|
StringBuilder sb2 = new StringBuilder ();
|
|
for (int j = 1; j < s.Value.Length; j += 2)
|
|
sb2.Append ((char)s.Value[j]);
|
|
sValue = sb2.ToString ();
|
|
} else {
|
|
if (s.Tag == 0x14)
|
|
sValue = Encoding.UTF7.GetString (s.Value);
|
|
else
|
|
sValue = Encoding.UTF8.GetString (s.Value);
|
|
// in some cases we must quote (") the value
|
|
// Note: this doesn't seems to conform to RFC2253
|
|
char[] specials = { ',', '+', '"', '\\', '<', '>', ';' };
|
|
if (quotes) {
|
|
if ((sValue.IndexOfAny (specials, 0, sValue.Length) > 0) ||
|
|
sValue.StartsWith (" ") || (sValue.EndsWith (" ")))
|
|
sValue = "\"" + sValue + "\"";
|
|
}
|
|
}
|
|
|
|
sb.Append (sValue);
|
|
|
|
// separator (not on last iteration)
|
|
if (k < entry.Count - 1)
|
|
sb.Append (", ");
|
|
}
|
|
}
|
|
|
|
static private X520.AttributeTypeAndValue GetAttributeFromOid (string attributeType)
|
|
{
|
|
string s = attributeType.ToUpper (CultureInfo.InvariantCulture).Trim ();
|
|
switch (s) {
|
|
case "C":
|
|
return new X520.CountryName ();
|
|
case "O":
|
|
return new X520.OrganizationName ();
|
|
case "OU":
|
|
return new X520.OrganizationalUnitName ();
|
|
case "CN":
|
|
return new X520.CommonName ();
|
|
case "L":
|
|
return new X520.LocalityName ();
|
|
case "S": // Microsoft
|
|
case "ST": // RFC2253
|
|
return new X520.StateOrProvinceName ();
|
|
case "E": // NOTE: Not part of RFC2253
|
|
return new X520.EmailAddress ();
|
|
case "DC": // RFC2247
|
|
return new X520.DomainComponent ();
|
|
case "UID": // RFC1274
|
|
return new X520.UserId ();
|
|
case "DNQUALIFIER":
|
|
return new X520.DnQualifier ();
|
|
case "T":
|
|
return new X520.Title ();
|
|
case "SN":
|
|
return new X520.Surname ();
|
|
case "G":
|
|
return new X520.GivenName ();
|
|
case "I":
|
|
return new X520.Initial ();
|
|
default:
|
|
if (s.StartsWith ("OID.")) {
|
|
// MUST support it but it OID may be without it
|
|
return new X520.Oid (s.Substring (4));
|
|
} else {
|
|
if (IsOid (s))
|
|
return new X520.Oid (s);
|
|
else
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
static private bool IsOid (string oid)
|
|
{
|
|
try {
|
|
ASN1 asn = ASN1Convert.FromOid (oid);
|
|
return (asn.Tag == 0x06);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// no quote processing
|
|
static private X520.AttributeTypeAndValue ReadAttribute (string value, ref int pos)
|
|
{
|
|
while ((value[pos] == ' ') && (pos < value.Length))
|
|
pos++;
|
|
|
|
// get '=' position in substring
|
|
int equal = value.IndexOf ('=', pos);
|
|
if (equal == -1) {
|
|
string msg = ("No attribute found.");
|
|
throw new FormatException (msg);
|
|
}
|
|
|
|
string s = value.Substring (pos, equal - pos);
|
|
X520.AttributeTypeAndValue atv = GetAttributeFromOid (s);
|
|
if (atv == null) {
|
|
string msg = ("Unknown attribute '{0}'.");
|
|
throw new FormatException (String.Format (msg, s));
|
|
}
|
|
pos = equal + 1; // skip the '='
|
|
return atv;
|
|
}
|
|
|
|
static private bool IsHex (char c)
|
|
{
|
|
if (Char.IsDigit (c))
|
|
return true;
|
|
char up = Char.ToUpper (c, CultureInfo.InvariantCulture);
|
|
return ((up >= 'A') && (up <= 'F'));
|
|
}
|
|
|
|
static string ReadHex (string value, ref int pos)
|
|
{
|
|
StringBuilder sb = new StringBuilder ();
|
|
// it is (at least an) 8 bits char
|
|
sb.Append (value[pos++]);
|
|
sb.Append (value[pos]);
|
|
// look ahead for a 16 bits char
|
|
if ((pos < value.Length - 4) && (value[pos+1] == '\\') && IsHex (value[pos+2])) {
|
|
pos += 2; // pass last char and skip \
|
|
sb.Append (value[pos++]);
|
|
sb.Append (value[pos]);
|
|
}
|
|
byte[] data = CryptoConvert.FromHex (sb.ToString ());
|
|
return Encoding.UTF8.GetString (data);
|
|
}
|
|
|
|
static private int ReadEscaped (StringBuilder sb, string value, int pos)
|
|
{
|
|
switch (value[pos]) {
|
|
case '\\':
|
|
case '"':
|
|
case '=':
|
|
case ';':
|
|
case '<':
|
|
case '>':
|
|
case '+':
|
|
case '#':
|
|
case ',':
|
|
sb.Append (value[pos]);
|
|
return pos;
|
|
default:
|
|
if (pos >= value.Length - 2) {
|
|
string msg = ("Malformed escaped value '{0}'.");
|
|
throw new FormatException (string.Format (msg, value.Substring (pos)));
|
|
}
|
|
// it's either a 8 bits or 16 bits char
|
|
sb.Append (ReadHex (value, ref pos));
|
|
return pos;
|
|
}
|
|
}
|
|
|
|
static private int ReadQuoted (StringBuilder sb, string value, int pos)
|
|
{
|
|
int original = pos;
|
|
while (pos <= value.Length) {
|
|
switch (value[pos]) {
|
|
case '"':
|
|
return pos;
|
|
case '\\':
|
|
return ReadEscaped (sb, value, pos);
|
|
default:
|
|
sb.Append (value[pos]);
|
|
pos++;
|
|
break;
|
|
}
|
|
}
|
|
string msg = ("Malformed quoted value '{0}'.");
|
|
throw new FormatException (string.Format (msg, value.Substring (original)));
|
|
}
|
|
|
|
static private string ReadValue (string value, ref int pos)
|
|
{
|
|
int original = pos;
|
|
StringBuilder sb = new StringBuilder ();
|
|
while (pos < value.Length) {
|
|
switch (value [pos]) {
|
|
case '\\':
|
|
pos = ReadEscaped (sb, value, ++pos);
|
|
break;
|
|
case '"':
|
|
pos = ReadQuoted (sb, value, ++pos);
|
|
break;
|
|
case '=':
|
|
case ';':
|
|
case '<':
|
|
case '>':
|
|
string msg =("Malformed value '{0}' contains '{1}' outside quotes.");
|
|
throw new FormatException (string.Format (msg, value.Substring (original), value[pos]));
|
|
case '+':
|
|
case '#':
|
|
throw new NotImplementedException ();
|
|
case ',':
|
|
pos++;
|
|
return sb.ToString ();
|
|
default:
|
|
sb.Append (value[pos]);
|
|
break;
|
|
}
|
|
pos++;
|
|
}
|
|
return sb.ToString ();
|
|
}
|
|
|
|
static public ASN1 FromString (string rdn)
|
|
{
|
|
if (rdn == null)
|
|
throw new ArgumentNullException ("rdn");
|
|
|
|
int pos = 0;
|
|
ASN1 asn1 = new ASN1 (0x30);
|
|
while (pos < rdn.Length) {
|
|
X520.AttributeTypeAndValue atv = ReadAttribute (rdn, ref pos);
|
|
atv.Value = ReadValue (rdn, ref pos);
|
|
|
|
ASN1 sequence = new ASN1 (0x31);
|
|
sequence.Add (atv.GetASN1 ());
|
|
asn1.Add (sequence);
|
|
}
|
|
return asn1;
|
|
}
|
|
}
|
|
}
|