// What etypes the KDC supports. Comma-separated strings. Null for all. // Please note native KDCs might use different names. privatestaticfinal String SUPPORTED_ETYPES
= System.getProperty("kdc.supported.enctypes");
// The native KDC privatefinal NativeKdc nativeKdc;
// The native KDC process private Process kdcProc = null;
// Under the hood.
// Principal db. principal -> pass. A case-insensitive TreeMap is used // so that even if the client provides a name with different case, the KDC // can still locate the principal and give back correct salt. private TreeMap<String,char[]> passwords = new TreeMap<>
(String.CASE_INSENSITIVE_ORDER);
// Non default salts. Precisely, there should be different salts for // different etypes, pretend they are the same at the moment. private TreeMap<String,String> salts = new TreeMap<>
(String.CASE_INSENSITIVE_ORDER);
// Non default s2kparams for newer etypes. Precisely, there should be // different s2kparams for different etypes, pretend they are the same // at the moment. private TreeMap<String,byte[]> s2kparamses = new TreeMap<>
(String.CASE_INSENSITIVE_ORDER);
// Alias for referrals. private TreeMap<String,KDC> aliasReferrals = new TreeMap<>
(String.CASE_INSENSITIVE_ORDER);
// Alias for local resolution. private TreeMap<String,PrincipalName> alias2Principals = new TreeMap<>
(String.CASE_INSENSITIVE_ORDER);
// Realm name private String realm; // KDC private String kdc; // Service port number privateint port; // The request/response job queue private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); // Options private Map<Option,Object> options = new HashMap<>(); // Realm-specific krb5.conf settings private List<String> conf = new ArrayList<>();
/** *ProcessesaTGS_REQandgeneratesaTGS_REP(orKRB_ERROR) *@paramintherequest *@returntheresponse *@throwsjava.lang.Exceptionforvariouserrors
*/ protectedbyte[] processTgsReq(byte[] in) throws Exception {
TGSReq tgsReq = new TGSReq(in);
PrincipalName service = tgsReq.reqBody.sname; if (options.containsKey(KDC.Option.RESP_NT)) {
service = new PrincipalName((int)options.get(KDC.Option.RESP_NT),
service.getNameStrings(), service.getRealm());
} try {
log(tgsReq.reqBody.cname + " sends TGS-REQ for " +
service + ", " + tgsReq.reqBody.kdcOptions);
KDCReqBody body = tgsReq.reqBody; int[] eTypes = filterSupported(KDCReqBodyDotEType(body)); if (eTypes.length == 0) { thrownew KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
} int e2 = eTypes[0]; // etype for outgoing session key int e3 = eTypes[0]; // etype for outgoing ticket
PAData[] pas = tgsReq.pAData;
Ticket tkt = null;
EncTicketPart etp = null;
PrincipalName cname = null; boolean allowForwardable = true; boolean isReferral = false; if (body.kdcOptions.get(KDCOptions.CANONICALIZE)) {
log("verifying referral for " +
body.sname.getNameString());
KDC referral = aliasReferrals.get(body.sname.getNameString()); if (referral != null) {
service = new PrincipalName(
PrincipalName.TGS_DEFAULT_SRV_NAME +
PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
referral.getRealm(), PrincipalName.KRB_NT_SRV_INST, this.getRealm());
log("referral to " +
referral.getRealm());
isReferral = true;
}
}
if (pas == null || pas.length == 0) { thrownew KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP);
} else {
PrincipalName forUserCName = null; for (PAData pa: pas) { if (pa.getType() == Krb5.PA_TGS_REQ) {
APReq apReq = new APReq(pa.getValue());
tkt = apReq.ticket; int te = tkt.encPart.getEType();
EncryptionKey kkey = keyForUser(tkt.sname, te, true); byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET);
DerInputStream derIn = new DerInputStream(bb);
DerValue der = derIn.getDerValue();
etp = new EncTicketPart(der.toByteArray()); // Finally, cname will be overwritten by PA-FOR-USER // if it exists.
cname = etp.cname;
log("presenting a ticket of "
+ etp.cname + " to " + tkt.sname);
} elseif (pa.getType() == Krb5.PA_FOR_USER) { if (options.containsKey(Option.ALLOW_S4U2SELF)) {
PAForUserEnc p4u = new PAForUserEnc( new DerValue(pa.getValue()), null);
forUserCName = p4u.name;
log("See PA_FOR_USER "
+ " in the name of " + p4u.name);
}
}
} if (forUserCName != null) {
List<String> names = (List<String>)
options.get(Option.ALLOW_S4U2SELF); if (!names.contains(cname.toString())) { // Mimic the normal KDC behavior. When a server is not // allowed to send S4U2self, do not send an error. // Instead, send a ticket which is useless later.
allowForwardable = false;
} elseif (options.get(Option.S4U2SELF_NOT_FORWARDABLE) == Boolean.TRUE) { // Requsted not forwardable
allowForwardable = false;
}
cname = forUserCName;
} if (tkt == null) { thrownew KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP);
}
}
// Session key for original ticket, TGT
EncryptionKey ckey = etp.key;
// Session key for session with the service
EncryptionKey key = generateRandomKey(e2);
// Check time, TODO
KerberosTime from = body.from;
KerberosTime till = body.till; if (from == null || from.isZero()) {
from = timeAfter(0);
} if (till == null) { thrownew KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO
} elseif (till.isZero()) {
till = timeAfter(DEFAULT_LIFETIME);
}
boolean[] bFlags = newboolean[Krb5.TKT_OPTS_MAX+1]; if (body.kdcOptions.get(KDCOptions.FORWARDABLE)
&& allowForwardable) {
List<String> sensitives = (List<String>)
options.get(Option.SENSITIVE_ACCOUNTS); if (sensitives != null && sensitives.contains(cname.toString())) { // Cannot make FORWARDABLE
} else {
bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true;
}
} // We do not request for addresses for FORWARDED tickets if (options.containsKey(Option.CHECK_ADDRESSES)
&& body.kdcOptions.get(KDCOptions.FORWARDED)
&& body.addresses != null) { thrownew KrbException(Krb5.KDC_ERR_BADOPTION);
} if (body.kdcOptions.get(KDCOptions.FORWARDED) ||
etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) {
bFlags[Krb5.TKT_OPTS_FORWARDED] = true;
} if (body.kdcOptions.get(KDCOptions.RENEWABLE)) {
bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; //renew = timeAfter(3600 * 24 * 7);
} if (body.kdcOptions.get(KDCOptions.PROXIABLE)) {
bFlags[Krb5.TKT_OPTS_PROXIABLE] = true;
} if (body.kdcOptions.get(KDCOptions.POSTDATED)) {
bFlags[Krb5.TKT_OPTS_POSTDATED] = true;
} if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) {
bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true;
} if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) { if (!options.containsKey(Option.ALLOW_S4U2PROXY)) { // Don't understand CNAME_IN_ADDL_TKT thrownew KrbException(Krb5.KDC_ERR_BADOPTION);
} else {
Map<String,List<String>> map = (Map<String,List<String>>)
options.get(Option.ALLOW_S4U2PROXY);
Ticket second = KDCReqBodyDotFirstAdditionalTicket(body);
EncryptionKey key2 = keyForUser(
second.sname, second.encPart.getEType(), true); byte[] bb = second.encPart.decrypt(key2, KeyUsage.KU_TICKET);
DerInputStream derIn = new DerInputStream(bb);
DerValue der = derIn.getDerValue();
EncTicketPart tktEncPart = new EncTicketPart(der.toByteArray()); if (!tktEncPart.flags.get(Krb5.TKT_OPTS_FORWARDABLE)
&& options.get(Option.S4U2SELF_ALLOW_NOT_FORWARDABLE) != Boolean.TRUE) { thrownew KrbException(Krb5.KDC_ERR_BADOPTION);
}
PrincipalName client = tktEncPart.cname;
log("and an additional ticket of "
+ client + " to " + second.sname); if (map.containsKey(cname.toString())) { if (map.get(cname.toString()).contains(service.toString())) {
log("S4U2proxy OK");
} else { thrownew KrbException(Krb5.KDC_ERR_BADOPTION);
}
} else { thrownew KrbException(Krb5.KDC_ERR_BADOPTION);
}
cname = client;
}
}
KerberosTime renewTill = etp.renewTill; if (renewTill != null && body.kdcOptions.get(KDCOptions.RENEW)) { // till should never pass renewTill if (till.greaterThan(renewTill)) {
till = renewTill;
} if (System.getProperty("test.set.null.renew") != null) { // Testing 8186576, see NullRenewUntil.java.
renewTill = null;
}
}
TicketFlags tFlags = new TicketFlags(bFlags);
EncTicketPart enc = new EncTicketPart(
tFlags,
key,
cname, new TransitedEncoding(1, newbyte[0]), // TODO
timeAfter(0),
from,
till, renewTill,
body.addresses != null ? body.addresses
: etp.caddr, null);
EncryptionKey skey = keyForUser(service, e3, true); if (skey == null) { thrownew KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO
}
Ticket t = new Ticket(
System.getProperty("test.kdc.diff.sname") != null ? new PrincipalName("xx" + service.toString()) :
service, new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET)
);
EncTGSRepPart enc_part = new EncTGSRepPart(
key, new LastReq(new LastReqEntry[] { new LastReqEntry(0, timeAfter(-10))
}),
body.getNonce(), // TODO: detect replay
timeAfter(3600 * 24), // Next 5 and last MUST be same with ticket
tFlags,
timeAfter(0),
from,
till, renewTill,
service,
body.addresses, null
);
EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(),
KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY);
TGSRep tgsRep = new TGSRep(null,
cname,
t,
edata);
System.out.println(" Return " + tgsRep.cname
+ " ticket for " + tgsRep.ticket.sname + ", flags "
+ tFlags);
PrincipalName service = asReq.reqBody.sname; if (options.containsKey(KDC.Option.RESP_NT)) {
service = new PrincipalName((int)options.get(KDC.Option.RESP_NT),
service.getNameStrings(),
Realm.getDefault());
} try {
log(asReq.reqBody.cname + " sends AS-REQ for " +
service + ", " + asReq.reqBody.kdcOptions);
KDCReqBody body = asReq.reqBody;
eTypes = filterSupported(KDCReqBodyDotEType(body)); if (eTypes.length == 0) { thrownew KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
} int eType = eTypes[0];
if (body.kdcOptions.get(KDCOptions.CANONICALIZE)) {
PrincipalName principal = alias2Principals.get(
body.cname.getNameString()); if (principal != null) {
body.cname = principal;
} else {
KDC referral = aliasReferrals.get(body.cname.getNameString()); if (referral != null) {
body.cname = new PrincipalName(
PrincipalName.TGS_DEFAULT_SRV_NAME,
PrincipalName.KRB_NT_SRV_INST,
referral.getRealm()); thrownew KrbException(Krb5.KRB_ERR_WRONG_REALM);
}
}
}
if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; boolean found = false; for (int i=0; i<eTypes.length; i++) { if (eTypes[i] == tgtEType) {
found = true; break;
}
} if (!found) { thrownew KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
}
skey = keyForUser(service, tgtEType, true);
} if (ckey == null) { thrownew KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
} if (skey == null) { thrownew KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO
}
// Session key
EncryptionKey key = generateRandomKey(eType); // Check time, TODO
KerberosTime from = body.from;
KerberosTime till = body.till;
KerberosTime rtime = body.rtime; if (from == null || from.isZero()) {
from = timeAfter(0);
} if (till == null) { thrownew KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO
} elseif (till.isZero()) {
till = timeAfter(DEFAULT_LIFETIME);
} elseif (till.greaterThan(timeAfter(24 * 3600))
&& System.getProperty("test.kdc.force.till") == null) { // If till is more than 1 day later, make it renewable
till = timeAfter(DEFAULT_LIFETIME);
body.kdcOptions.set(KDCOptions.RENEWABLE, true); if (rtime == null) rtime = till;
} if (rtime == null && body.kdcOptions.get(KDCOptions.RENEWABLE)) {
rtime = timeAfter(DEFAULT_RENEWTIME);
} //body.from boolean[] bFlags = newboolean[Krb5.TKT_OPTS_MAX+1]; if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) {
List<String> sensitives = (List<String>)
options.get(Option.SENSITIVE_ACCOUNTS); if (sensitives != null
&& sensitives.contains(body.cname.toString())) { // Cannot make FORWARDABLE
} else {
bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true;
}
} if (body.kdcOptions.get(KDCOptions.RENEWABLE)) {
bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; //renew = timeAfter(3600 * 24 * 7);
} if (body.kdcOptions.get(KDCOptions.PROXIABLE)) {
bFlags[Krb5.TKT_OPTS_PROXIABLE] = true;
} if (body.kdcOptions.get(KDCOptions.POSTDATED)) {
bFlags[Krb5.TKT_OPTS_POSTDATED] = true;
} if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) {
bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true;
}
bFlags[Krb5.TKT_OPTS_INITIAL] = true; if (System.getProperty("test.kdc.always.enc.pa.rep") != null) {
bFlags[Krb5.TKT_OPTS_ENC_PA_REP] = true;
}
// Creating PA-DATA
DerValue[] pas2 = null, pas = null; if (options.containsKey(KDC.Option.DUP_ETYPE)) { int n = (Integer)options.get(KDC.Option.DUP_ETYPE); switch (n) { case1: // customer's case in 7067974
pas2 = new DerValue[] { new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), new DerValue(new ETypeInfo2( 1, realm, newbyte[]{1}).asn1Encode()),
};
pas = new DerValue[] { new DerValue(new ETypeInfo(1, null).asn1Encode()), new DerValue(new ETypeInfo(1, "").asn1Encode()), new DerValue(new ETypeInfo(1, realm).asn1Encode()),
}; break; case2: // we still reject non-null s2kparams and prefer E2 over E
pas2 = new DerValue[] { new DerValue(new ETypeInfo2( 1, realm, newbyte[]{1}).asn1Encode()), new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), new DerValue(new ETypeInfo2(1, "", null).asn1Encode()),
};
pas = new DerValue[] { new DerValue(new ETypeInfo(1, realm).asn1Encode()), new DerValue(new ETypeInfo(1, null).asn1Encode()), new DerValue(new ETypeInfo(1, "").asn1Encode()),
}; break; case3: // but only E is wrong
pas = new DerValue[] { new DerValue(new ETypeInfo(1, realm).asn1Encode()), new DerValue(new ETypeInfo(1, null).asn1Encode()), new DerValue(new ETypeInfo(1, "").asn1Encode()),
}; break; case4: // we also ignore rc4-hmac
pas = new DerValue[] { new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()), new DerValue(new ETypeInfo(1, null).asn1Encode()), new DerValue(new ETypeInfo(1, "").asn1Encode()),
}; break; case5: // "" should be wrong, but we accept it now // See s.s.k.internal.PAData$SaltAndParams
pas = new DerValue[] { new DerValue(new ETypeInfo(1, "").asn1Encode()), new DerValue(new ETypeInfo(1, null).asn1Encode()),
}; break;
}
} else { int[] epas = eTypes; if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { for (int i=1; i<epas.length; i++) { if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) {
epas[i] = epas[0];
epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; break;
}
};
} elseif (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) {
epas = newint[] { eTypes[0] };
}
pas2 = new DerValue[epas.length]; for (int i=0; i<epas.length; i++) {
pas2[i] = new DerValue(new ETypeInfo2(
epas[i],
epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? null : getSalt(body.cname),
getParams(body.cname, epas[i])).asn1Encode());
} boolean allOld = true; for (int i: eTypes) { if (i >= EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 &&
i != EncryptedData.ETYPE_ARCFOUR_HMAC) {
allOld = false; break;
}
} if (allOld) {
pas = new DerValue[epas.length]; for (int i=0; i<epas.length; i++) {
pas[i] = new DerValue(new ETypeInfo(
epas[i],
epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? null : getSalt(body.cname)
).asn1Encode());
}
}
}
DerOutputStream eid; if (pas2 != null) {
eid = new DerOutputStream();
eid.putSequence(pas2);
outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray()));
} if (pas != null) {
eid = new DerOutputStream();
eid.putSequence(pas);
outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray()));
}
PAData[] inPAs = asReq.pAData;
List<PAData> enc_outPAs = new ArrayList<>();
byte[] paEncTimestamp = null; if (inPAs != null) { for (PAData inPA : inPAs) { if (inPA.getType() == Krb5.PA_ENC_TIMESTAMP) {
paEncTimestamp = inPA.getValue();
}
}
}
if (paEncTimestamp == null) {
Object preauth = options.get(Option.PREAUTH_REQUIRED); if (preauth == null || preauth.equals(Boolean.TRUE)) { thrownew KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED);
}
} else {
EncryptionKey pakey = null; try {
EncryptedData data = newEncryptedData( new DerValue(paEncTimestamp));
pakey = keyForUser(body.cname, data.getEType(), false);
data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS);
} catch (Exception e) {
KrbException ke = new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED);
ke.initCause(e); throw ke;
}
bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; for (PAData pa : inPAs) { if (pa.getType() == Krb5.PA_REQ_ENC_PA_REP) {
Checksum ckSum = new Checksum(
Checksum.CKSUMTYPE_HMAC_SHA1_96_AES128,
asReqbytes, ckey, KeyUsage.KU_AS_REQ);
enc_outPAs.add(new PAData(Krb5.PA_REQ_ENC_PA_REP,
ckSum.asn1Encode()));
bFlags[Krb5.TKT_OPTS_ENC_PA_REP] = true; break;
}
}
}
TicketFlags tFlags = new TicketFlags(bFlags);
EncTicketPart enc = new EncTicketPart(
tFlags,
key,
body.cname, new TransitedEncoding(1, newbyte[0]),
timeAfter(0),
from,
till, rtime,
body.addresses, null);
Ticket t = new Ticket(
service, new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET)
);
EncASRepPart enc_part = new EncASRepPart(
key, new LastReq(new LastReqEntry[]{ new LastReqEntry(0, timeAfter(-10))
}),
body.getNonce(), // TODO: detect replay?
timeAfter(3600 * 24), // Next 5 and last MUST be same with ticket
tFlags,
timeAfter(0),
from,
till, rtime,
service,
body.addresses,
enc_outPAs.toArray(new PAData[enc_outPAs.size()])
);
EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(),
KeyUsage.KU_ENC_AS_REP_PART);
ASRep asRep = new ASRep(
outPAs.toArray(new PAData[outPAs.size()]),
body.cname,
t,
edata);
DerOutputStream out = new DerOutputStream();
out.write(DerValue.createTag(DerValue.TAG_APPLICATION, true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); byte[] result = out.toByteArray();
// Added feature: // Write the current issuing TGT into a ccache file specified // by the system property below.
String ccache = System.getProperty("test.kdc.save.ccache"); if (ccache != null) {
asRep.encKDCRepPart = enc_part;
sun.security.krb5.internal.ccache.Credentials credentials = new sun.security.krb5.internal.ccache.Credentials(asRep);
CredentialsCache cache =
CredentialsCache.create(asReq.reqBody.cname, ccache); if (cache == null) { thrownew IOException("Unable to create the cache file " +
ccache);
}
cache.update(credentials);
cache.save();
}
publicstatic KDC startKDC(final String host, final String krbConfFileName, final String realm, final Map<String, String> principals, final String ktab, final KtabMode mode) {
// Add principals if (principals != null) {
principals.forEach((name, password) -> { if (password == null || password.isEmpty()) {
System.out.println(String.format( "KDC:add a principal '%s' with a random " + "password", name));
kdc.addPrincipalRandKey(name);
} else {
System.out.println(String.format( "KDC:add a principal '%s' with '%s' password",
name, password));
kdc.addPrincipal(name, password.toCharArray());
}
});
}
// Create or append keys to existing keytab file if (ktab != null) {
File ktabFile = new File(ktab); switch(mode) { case APPEND: if (ktabFile.exists()) {
System.out.println(String.format( "KDC:append keys to an exising keytab "
+ "file %s", ktab));
kdc.appendKtab(ktab);
} else {
System.out.println(String.format( "KDC:create a new keytab file %s", ktab));
kdc.writeKtab(ktab);
} break; case EXISTING:
System.out.println(String.format( "KDC:use an existing keytab file %s", ktab)); break; default: thrownew RuntimeException(String.format( "KDC:unsupported keytab mode: %s", mode));
}
}
System.out.println(String.format( "KDC: started on %s:%s with '%s' realm",
host, kdc.getPort(), realm));
} catch (Exception e) { thrownew RuntimeException("KDC: unexpected exception", e);
}
return kdc;
}
/** *HelperclasstoencapsulateajobinaKDC.
*/ privatestaticclass Job { byte[] token; // The received request at creation time and // the response at send time
Socket s; // The TCP socket from where the request comes
DataOutputStream out; // The OutputStream of the TCP socket
DatagramSocket s2; // The UDP socket from where the request comes
DatagramPacket dp; // The incoming UDP datagram packet boolean useTCP; // Whether TCP or UDP is used
// MIT krb5 KDC. Make your own exploded (install == false), or // "make install" into nativePath (install == true). staticclass MIT extends NativeKdc {
privateboolean install; // "make install" or "make"
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.