Annotation of embedaddon/strongswan/src/libstrongswan/plugins/constraints/constraints_validator.c, revision 1.1
1.1 ! misho 1: /*
! 2: * Copyright (C) 2010 Martin Willi
! 3: * Copyright (C) 2010 revosec AG
! 4: *
! 5: * This program is free software; you can redistribute it and/or modify it
! 6: * under the terms of the GNU General Public License as published by the
! 7: * Free Software Foundation; either version 2 of the License, or (at your
! 8: * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
! 9: *
! 10: * This program is distributed in the hope that it will be useful, but
! 11: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
! 12: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
! 13: * for more details.
! 14: */
! 15:
! 16: #include "constraints_validator.h"
! 17:
! 18: #include <utils/debug.h>
! 19: #include <asn1/asn1.h>
! 20: #include <collections/linked_list.h>
! 21: #include <credentials/certificates/x509.h>
! 22:
! 23: typedef struct private_constraints_validator_t private_constraints_validator_t;
! 24:
! 25: /**
! 26: * Private data of an constraints_validator_t object.
! 27: */
! 28: struct private_constraints_validator_t {
! 29:
! 30: /**
! 31: * Public constraints_validator_t interface.
! 32: */
! 33: constraints_validator_t public;
! 34: };
! 35:
! 36: /**
! 37: * Check pathlen constraint of issuer certificate
! 38: */
! 39: static bool check_pathlen(x509_t *issuer, int pathlen)
! 40: {
! 41: u_int pathlen_constraint;
! 42:
! 43: pathlen_constraint = issuer->get_constraint(issuer, X509_PATH_LEN);
! 44: if (pathlen_constraint != X509_NO_CONSTRAINT &&
! 45: pathlen > pathlen_constraint)
! 46: {
! 47: DBG1(DBG_CFG, "path length of %d violates constraint of %d",
! 48: pathlen, pathlen_constraint);
! 49: return FALSE;
! 50: }
! 51: return TRUE;
! 52: }
! 53:
! 54: /**
! 55: * Check if a FQDN constraint matches
! 56: */
! 57: static bool fqdn_matches(identification_t *constraint, identification_t *id)
! 58: {
! 59: chunk_t c, i, diff;
! 60:
! 61: c = constraint->get_encoding(constraint);
! 62: i = id->get_encoding(id);
! 63:
! 64: if (!c.len || i.len < c.len)
! 65: {
! 66: return FALSE;
! 67: }
! 68: diff = chunk_create(i.ptr, i.len - c.len);
! 69: if (!chunk_equals(c, chunk_skip(i, diff.len)))
! 70: {
! 71: return FALSE;
! 72: }
! 73: if (!diff.len)
! 74: {
! 75: return TRUE;
! 76: }
! 77: if (c.ptr[0] == '.' || diff.ptr[diff.len - 1] == '.')
! 78: {
! 79: return TRUE;
! 80: }
! 81: return FALSE;
! 82: }
! 83:
! 84: /**
! 85: * Check if a RFC822 constraint matches
! 86: */
! 87: static bool email_matches(identification_t *constraint, identification_t *id)
! 88: {
! 89: chunk_t c, i, diff;
! 90:
! 91: c = constraint->get_encoding(constraint);
! 92: i = id->get_encoding(id);
! 93:
! 94: if (!c.len || i.len < c.len)
! 95: {
! 96: return FALSE;
! 97: }
! 98: if (memchr(c.ptr, '@', c.len))
! 99: { /* constraint is a full email address */
! 100: return chunk_equals(c, i);
! 101: }
! 102: diff = chunk_create(i.ptr, i.len - c.len);
! 103: if (!diff.len || !chunk_equals(c, chunk_skip(i, diff.len)))
! 104: {
! 105: return FALSE;
! 106: }
! 107: if (c.ptr[0] == '.')
! 108: { /* constraint is domain, suffix match */
! 109: return TRUE;
! 110: }
! 111: if (diff.ptr[diff.len - 1] == '@')
! 112: { /* constraint is host specific, only username can be appended */
! 113: return TRUE;
! 114: }
! 115: return FALSE;
! 116: }
! 117:
! 118: /**
! 119: * Check if a DN constraint matches (RDN prefix match)
! 120: */
! 121: static bool dn_matches(identification_t *constraint, identification_t *id)
! 122: {
! 123: enumerator_t *ec, *ei;
! 124: id_part_t pc, pi;
! 125: chunk_t cc, ci;
! 126: bool match = TRUE;
! 127:
! 128: ec = constraint->create_part_enumerator(constraint);
! 129: ei = id->create_part_enumerator(id);
! 130: while (ec->enumerate(ec, &pc, &cc))
! 131: {
! 132: if (!ei->enumerate(ei, &pi, &ci) ||
! 133: pi != pc || !chunk_equals(cc, ci))
! 134: {
! 135: match = FALSE;
! 136: break;
! 137: }
! 138: }
! 139: ec->destroy(ec);
! 140: ei->destroy(ei);
! 141:
! 142: return match;
! 143: }
! 144:
! 145: /**
! 146: * Check if a certificate matches to a NameConstraint
! 147: */
! 148: static bool name_constraint_matches(identification_t *constraint,
! 149: certificate_t *cert, bool permitted)
! 150: {
! 151: x509_t *x509 = (x509_t*)cert;
! 152: enumerator_t *enumerator;
! 153: identification_t *id;
! 154: id_type_t type;
! 155: bool matches = permitted;
! 156:
! 157: type = constraint->get_type(constraint);
! 158: if (type == ID_DER_ASN1_DN)
! 159: {
! 160: matches = dn_matches(constraint, cert->get_subject(cert));
! 161: if (matches != permitted)
! 162: {
! 163: return matches;
! 164: }
! 165: }
! 166:
! 167: enumerator = x509->create_subjectAltName_enumerator(x509);
! 168: while (enumerator->enumerate(enumerator, &id))
! 169: {
! 170: if (id->get_type(id) == type)
! 171: {
! 172: switch (type)
! 173: {
! 174: case ID_FQDN:
! 175: matches = fqdn_matches(constraint, id);
! 176: break;
! 177: case ID_RFC822_ADDR:
! 178: matches = email_matches(constraint, id);
! 179: break;
! 180: case ID_DER_ASN1_DN:
! 181: matches = dn_matches(constraint, id);
! 182: break;
! 183: default:
! 184: DBG1(DBG_CFG, "%N NameConstraint matching not implemented",
! 185: id_type_names, type);
! 186: matches = FALSE;
! 187: break;
! 188: }
! 189: }
! 190: if (matches != permitted)
! 191: {
! 192: break;
! 193: }
! 194: }
! 195: enumerator->destroy(enumerator);
! 196:
! 197: return matches;
! 198: }
! 199:
! 200: /**
! 201: * Check if a permitted or excluded NameConstraint has been inherited to sub-CA
! 202: */
! 203: static bool name_constraint_inherited(identification_t *constraint,
! 204: x509_t *x509, bool permitted)
! 205: {
! 206: enumerator_t *enumerator;
! 207: identification_t *id, *a, *b;
! 208: bool inherited = FALSE;
! 209: id_type_t type;
! 210:
! 211: if (!(x509->get_flags(x509) & X509_CA))
! 212: { /* not a sub-CA, not required */
! 213: return TRUE;
! 214: }
! 215:
! 216: type = constraint->get_type(constraint);
! 217: enumerator = x509->create_name_constraint_enumerator(x509, permitted);
! 218: while (enumerator->enumerate(enumerator, &id))
! 219: {
! 220: if (id->get_type(id) == type)
! 221: {
! 222: if (permitted)
! 223: { /* permitted constraint can be narrowed */
! 224: a = constraint;
! 225: b = id;
! 226: }
! 227: else
! 228: { /* excluded constraint can be widened */
! 229: a = id;
! 230: b = constraint;
! 231: }
! 232: switch (type)
! 233: {
! 234: case ID_FQDN:
! 235: inherited = fqdn_matches(a, b);
! 236: break;
! 237: case ID_RFC822_ADDR:
! 238: inherited = email_matches(a, b);
! 239: break;
! 240: case ID_DER_ASN1_DN:
! 241: inherited = dn_matches(a, b);
! 242: break;
! 243: default:
! 244: DBG1(DBG_CFG, "%N NameConstraint matching not implemented",
! 245: id_type_names, type);
! 246: inherited = FALSE;
! 247: break;
! 248: }
! 249: }
! 250: if (inherited)
! 251: {
! 252: break;
! 253: }
! 254: }
! 255: enumerator->destroy(enumerator);
! 256: return inherited;
! 257: }
! 258:
! 259: /**
! 260: * Check name constraints
! 261: */
! 262: static bool check_name_constraints(certificate_t *subject, x509_t *issuer)
! 263: {
! 264: enumerator_t *enumerator;
! 265: identification_t *constraint;
! 266:
! 267: enumerator = issuer->create_name_constraint_enumerator(issuer, TRUE);
! 268: while (enumerator->enumerate(enumerator, &constraint))
! 269: {
! 270: if (!name_constraint_matches(constraint, subject, TRUE))
! 271: {
! 272: DBG1(DBG_CFG, "certificate '%Y' does not match permitted name "
! 273: "constraint '%Y'", subject->get_subject(subject), constraint);
! 274: enumerator->destroy(enumerator);
! 275: return FALSE;
! 276: }
! 277: if (!name_constraint_inherited(constraint, (x509_t*)subject, TRUE))
! 278: {
! 279: DBG1(DBG_CFG, "intermediate CA '%Y' does not inherit permitted name "
! 280: "constraint '%Y'", subject->get_subject(subject), constraint);
! 281: enumerator->destroy(enumerator);
! 282: return FALSE;
! 283: }
! 284: }
! 285: enumerator->destroy(enumerator);
! 286:
! 287: enumerator = issuer->create_name_constraint_enumerator(issuer, FALSE);
! 288: while (enumerator->enumerate(enumerator, &constraint))
! 289: {
! 290: if (name_constraint_matches(constraint, subject, FALSE))
! 291: {
! 292: DBG1(DBG_CFG, "certificate '%Y' matches excluded name "
! 293: "constraint '%Y'", subject->get_subject(subject), constraint);
! 294: enumerator->destroy(enumerator);
! 295: return FALSE;
! 296: }
! 297: if (!name_constraint_inherited(constraint, (x509_t*)subject, FALSE))
! 298: {
! 299: DBG1(DBG_CFG, "intermediate CA '%Y' does not inherit excluded name "
! 300: "constraint '%Y'", subject->get_subject(subject), constraint);
! 301: enumerator->destroy(enumerator);
! 302: return FALSE;
! 303: }
! 304: }
! 305: enumerator->destroy(enumerator);
! 306: return TRUE;
! 307: }
! 308:
! 309: /**
! 310: * Special OID for anyPolicy
! 311: */
! 312: static chunk_t any_policy = chunk_from_chars(0x55,0x1d,0x20,0x00);
! 313:
! 314: /**
! 315: * Check if an issuer certificate has a given policy OID
! 316: */
! 317: static bool has_policy(x509_t *issuer, chunk_t oid)
! 318: {
! 319: x509_policy_mapping_t *mapping;
! 320: x509_cert_policy_t *policy;
! 321: enumerator_t *enumerator;
! 322:
! 323: enumerator = issuer->create_cert_policy_enumerator(issuer);
! 324: while (enumerator->enumerate(enumerator, &policy))
! 325: {
! 326: if (chunk_equals(oid, policy->oid) ||
! 327: chunk_equals(any_policy, policy->oid))
! 328: {
! 329: enumerator->destroy(enumerator);
! 330: return TRUE;
! 331: }
! 332: }
! 333: enumerator->destroy(enumerator);
! 334:
! 335: /* fall back to a mapped policy */
! 336: enumerator = issuer->create_policy_mapping_enumerator(issuer);
! 337: while (enumerator->enumerate(enumerator, &mapping))
! 338: {
! 339: if (chunk_equals(mapping->subject, oid))
! 340: {
! 341: enumerator->destroy(enumerator);
! 342: return TRUE;
! 343: }
! 344: }
! 345: enumerator->destroy(enumerator);
! 346: return FALSE;
! 347: }
! 348:
! 349: /**
! 350: * Check certificatePolicies.
! 351: */
! 352: static bool check_policy(x509_t *subject, x509_t *issuer)
! 353: {
! 354: certificate_t *cert = (certificate_t*)subject;
! 355: x509_policy_mapping_t *mapping;
! 356: x509_cert_policy_t *policy;
! 357: enumerator_t *enumerator;
! 358: char *oid;
! 359:
! 360: /* verify if policyMappings in subject are valid */
! 361: enumerator = subject->create_policy_mapping_enumerator(subject);
! 362: while (enumerator->enumerate(enumerator, &mapping))
! 363: {
! 364: if (!has_policy(issuer, mapping->issuer))
! 365: {
! 366: oid = asn1_oid_to_string(mapping->issuer);
! 367: DBG1(DBG_CFG, "certificate '%Y' maps policy from %s, but issuer "
! 368: "misses it", cert->get_subject(cert), oid);
! 369: free(oid);
! 370: enumerator->destroy(enumerator);
! 371: return FALSE;
! 372: }
! 373: }
! 374: enumerator->destroy(enumerator);
! 375:
! 376: enumerator = subject->create_cert_policy_enumerator(subject);
! 377: while (enumerator->enumerate(enumerator, &policy))
! 378: {
! 379: if (!has_policy(issuer, policy->oid))
! 380: {
! 381: oid = asn1_oid_to_string(policy->oid);
! 382: DBG1(DBG_CFG, "policy %s missing in issuing certificate '%Y'",
! 383: oid, cert->get_issuer(cert));
! 384: free(oid);
! 385: enumerator->destroy(enumerator);
! 386: return FALSE;
! 387: }
! 388: }
! 389: enumerator->destroy(enumerator);
! 390:
! 391: return TRUE;
! 392: }
! 393:
! 394: /**
! 395: * Check if a given policy is valid under a trustchain
! 396: */
! 397: static bool is_policy_valid(linked_list_t *chain, chunk_t oid)
! 398: {
! 399: x509_policy_mapping_t *mapping;
! 400: x509_cert_policy_t *policy;
! 401: x509_t *issuer;
! 402: enumerator_t *issuers, *policies, *mappings;
! 403: bool found = TRUE;
! 404:
! 405: issuers = chain->create_enumerator(chain);
! 406: while (issuers->enumerate(issuers, &issuer))
! 407: {
! 408: int maxmap = 8;
! 409:
! 410: while (found)
! 411: {
! 412: found = FALSE;
! 413:
! 414: policies = issuer->create_cert_policy_enumerator(issuer);
! 415: while (policies->enumerate(policies, &policy))
! 416: {
! 417: if (chunk_equals(oid, policy->oid) ||
! 418: chunk_equals(any_policy, policy->oid))
! 419: {
! 420: found = TRUE;
! 421: break;
! 422: }
! 423: }
! 424: policies->destroy(policies);
! 425: if (found)
! 426: {
! 427: break;
! 428: }
! 429: /* fall back to a mapped policy */
! 430: mappings = issuer->create_policy_mapping_enumerator(issuer);
! 431: while (mappings->enumerate(mappings, &mapping))
! 432: {
! 433: if (chunk_equals(mapping->subject, oid))
! 434: {
! 435: oid = mapping->issuer;
! 436: found = TRUE;
! 437: break;
! 438: }
! 439: }
! 440: mappings->destroy(mappings);
! 441: if (--maxmap == 0)
! 442: {
! 443: found = FALSE;
! 444: break;
! 445: }
! 446: }
! 447: if (!found)
! 448: {
! 449: break;
! 450: }
! 451: }
! 452: issuers->destroy(issuers);
! 453:
! 454: return found;
! 455: }
! 456:
! 457: /**
! 458: * Check len certificates in trustchain for inherited policies
! 459: */
! 460: static bool has_policy_chain(linked_list_t *chain, x509_t *subject, int len)
! 461: {
! 462: enumerator_t *enumerator;
! 463: x509_t *issuer;
! 464: bool valid = TRUE;
! 465:
! 466: enumerator = chain->create_enumerator(chain);
! 467: while (len-- > 0 && enumerator->enumerate(enumerator, &issuer))
! 468: {
! 469: if (!check_policy(subject, issuer))
! 470: {
! 471: valid = FALSE;
! 472: break;
! 473: }
! 474: subject = issuer;
! 475: }
! 476: enumerator->destroy(enumerator);
! 477: return valid;
! 478: }
! 479:
! 480: /**
! 481: * Check len certificates in trustchain to have no policyMappings
! 482: */
! 483: static bool has_no_policy_mapping(linked_list_t *chain, int len)
! 484: {
! 485: enumerator_t *enumerator, *mappings;
! 486: x509_policy_mapping_t *mapping;
! 487: certificate_t *cert;
! 488: x509_t *x509;
! 489: bool valid = TRUE;
! 490:
! 491: enumerator = chain->create_enumerator(chain);
! 492: while (len-- > 0 && enumerator->enumerate(enumerator, &x509))
! 493: {
! 494: mappings = x509->create_policy_mapping_enumerator(x509);
! 495: valid = !mappings->enumerate(mappings, &mapping);
! 496: mappings->destroy(mappings);
! 497: if (!valid)
! 498: {
! 499: cert = (certificate_t*)x509;
! 500: DBG1(DBG_CFG, "found policyMapping in certificate '%Y', but "
! 501: "inhibitPolicyMapping in effect", cert->get_subject(cert));
! 502: break;
! 503: }
! 504: }
! 505: enumerator->destroy(enumerator);
! 506: return valid;
! 507: }
! 508:
! 509: /**
! 510: * Check len certificates in trustchain to have no anyPolicies
! 511: */
! 512: static bool has_no_any_policy(linked_list_t *chain, int len)
! 513: {
! 514: enumerator_t *enumerator, *policies;
! 515: x509_cert_policy_t *policy;
! 516: certificate_t *cert;
! 517: x509_t *x509;
! 518: bool valid = TRUE;
! 519:
! 520: enumerator = chain->create_enumerator(chain);
! 521: while (len-- > 0 && enumerator->enumerate(enumerator, &x509))
! 522: {
! 523: policies = x509->create_cert_policy_enumerator(x509);
! 524: while (policies->enumerate(policies, &policy))
! 525: {
! 526: if (chunk_equals(policy->oid, any_policy))
! 527: {
! 528: cert = (certificate_t*)x509;
! 529: DBG1(DBG_CFG, "found anyPolicy in certificate '%Y', but "
! 530: "inhibitAnyPolicy in effect", cert->get_subject(cert));
! 531: valid = FALSE;
! 532: break;
! 533: }
! 534: }
! 535: policies->destroy(policies);
! 536: }
! 537: enumerator->destroy(enumerator);
! 538: return valid;
! 539: }
! 540:
! 541: /**
! 542: * Check requireExplicitPolicy and inhibitPolicyMapping constraints
! 543: */
! 544: static bool check_policy_constraints(x509_t *issuer, u_int pathlen,
! 545: auth_cfg_t *auth)
! 546: {
! 547: certificate_t *subject;
! 548: bool valid = TRUE;
! 549:
! 550: subject = auth->get(auth, AUTH_RULE_SUBJECT_CERT);
! 551: if (subject)
! 552: {
! 553: if (subject->get_type(subject) == CERT_X509)
! 554: {
! 555: x509_cert_policy_t *policy;
! 556: enumerator_t *enumerator;
! 557: linked_list_t *chain;
! 558: certificate_t *cert;
! 559: auth_rule_t rule;
! 560: x509_t *x509;
! 561: int len = 0;
! 562: u_int expl, inh;
! 563: char *oid;
! 564:
! 565: /* prepare trustchain to validate */
! 566: chain = linked_list_create();
! 567: enumerator = auth->create_enumerator(auth);
! 568: while (enumerator->enumerate(enumerator, &rule, &cert))
! 569: {
! 570: if (rule == AUTH_RULE_IM_CERT &&
! 571: cert->get_type(cert) == CERT_X509)
! 572: {
! 573: chain->insert_last(chain, cert);
! 574: }
! 575: }
! 576: enumerator->destroy(enumerator);
! 577: chain->insert_last(chain, issuer);
! 578:
! 579: /* search for requireExplicitPolicy constraints */
! 580: enumerator = chain->create_enumerator(chain);
! 581: while (enumerator->enumerate(enumerator, &x509))
! 582: {
! 583: expl = x509->get_constraint(x509, X509_REQUIRE_EXPLICIT_POLICY);
! 584: if (expl != X509_NO_CONSTRAINT)
! 585: {
! 586: if (!has_policy_chain(chain, (x509_t*)subject, len - expl))
! 587: {
! 588: valid = FALSE;
! 589: break;
! 590: }
! 591: }
! 592: len++;
! 593: }
! 594: enumerator->destroy(enumerator);
! 595:
! 596: /* search for inhibitPolicyMapping/inhibitAnyPolicy constraints */
! 597: len = 0;
! 598: chain->insert_first(chain, subject);
! 599: enumerator = chain->create_enumerator(chain);
! 600: while (enumerator->enumerate(enumerator, &x509))
! 601: {
! 602: inh = x509->get_constraint(x509, X509_INHIBIT_POLICY_MAPPING);
! 603: if (inh != X509_NO_CONSTRAINT)
! 604: {
! 605: if (!has_no_policy_mapping(chain, len - inh))
! 606: {
! 607: valid = FALSE;
! 608: break;
! 609: }
! 610: }
! 611: inh = x509->get_constraint(x509, X509_INHIBIT_ANY_POLICY);
! 612: if (inh != X509_NO_CONSTRAINT)
! 613: {
! 614: if (!has_no_any_policy(chain, len - inh))
! 615: {
! 616: valid = FALSE;
! 617: break;
! 618: }
! 619: }
! 620: len++;
! 621: }
! 622: enumerator->destroy(enumerator);
! 623:
! 624: if (valid)
! 625: {
! 626: x509 = (x509_t*)subject;
! 627:
! 628: enumerator = x509->create_cert_policy_enumerator(x509);
! 629: while (enumerator->enumerate(enumerator, &policy))
! 630: {
! 631: oid = asn1_oid_to_string(policy->oid);
! 632: if (oid)
! 633: {
! 634: if (is_policy_valid(chain, policy->oid))
! 635: {
! 636: auth->add(auth, AUTH_RULE_CERT_POLICY, oid);
! 637: }
! 638: else
! 639: {
! 640: DBG1(DBG_CFG, "certificate policy %s for '%Y' "
! 641: "not allowed by trustchain, ignored",
! 642: oid, subject->get_subject(subject));
! 643: free(oid);
! 644: }
! 645: }
! 646: }
! 647: enumerator->destroy(enumerator);
! 648: }
! 649: chain->destroy(chain);
! 650: }
! 651: }
! 652: return valid;
! 653: }
! 654:
! 655: METHOD(cert_validator_t, validate, bool,
! 656: private_constraints_validator_t *this, certificate_t *subject,
! 657: certificate_t *issuer, bool online, u_int pathlen, bool anchor,
! 658: auth_cfg_t *auth)
! 659: {
! 660: if (issuer->get_type(issuer) == CERT_X509 &&
! 661: subject->get_type(subject) == CERT_X509)
! 662: {
! 663: if (!check_pathlen((x509_t*)issuer, pathlen))
! 664: {
! 665: lib->credmgr->call_hook(lib->credmgr, CRED_HOOK_EXCEEDED_PATH_LEN,
! 666: subject);
! 667: return FALSE;
! 668: }
! 669: if (!check_name_constraints(subject, (x509_t*)issuer))
! 670: {
! 671: lib->credmgr->call_hook(lib->credmgr, CRED_HOOK_POLICY_VIOLATION,
! 672: subject);
! 673: return FALSE;
! 674: }
! 675: if (anchor)
! 676: {
! 677: if (!check_policy_constraints((x509_t*)issuer, pathlen, auth))
! 678: {
! 679: lib->credmgr->call_hook(lib->credmgr,
! 680: CRED_HOOK_POLICY_VIOLATION, issuer);
! 681: return FALSE;
! 682: }
! 683: }
! 684: }
! 685: return TRUE;
! 686: }
! 687:
! 688: METHOD(constraints_validator_t, destroy, void,
! 689: private_constraints_validator_t *this)
! 690: {
! 691: free(this);
! 692: }
! 693:
! 694: /**
! 695: * See header
! 696: */
! 697: constraints_validator_t *constraints_validator_create()
! 698: {
! 699: private_constraints_validator_t *this;
! 700:
! 701: INIT(this,
! 702: .public = {
! 703: .validator.validate = _validate,
! 704: .destroy = _destroy,
! 705: },
! 706: );
! 707:
! 708: return &this->public;
! 709: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>