Annotation of embedaddon/strongswan/src/libstrongswan/crypto/pkcs5.c, revision 1.1
1.1 ! misho 1: /*
! 2: * Copyright (C) 2012-2013 Tobias Brunner
! 3: * HSR Hochschule fuer Technik Rapperswil
! 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 "pkcs5.h"
! 17:
! 18: #include <utils/debug.h>
! 19: #include <asn1/oid.h>
! 20: #include <asn1/asn1.h>
! 21: #include <asn1/asn1_parser.h>
! 22: #include <credentials/containers/pkcs12.h>
! 23:
! 24: typedef struct private_pkcs5_t private_pkcs5_t;
! 25:
! 26: /**
! 27: * Private data of a pkcs5_t object
! 28: */
! 29: struct private_pkcs5_t {
! 30:
! 31: /**
! 32: * Implements pkcs5_t.
! 33: */
! 34: pkcs5_t public;
! 35:
! 36: /**
! 37: * Salt used during encryption
! 38: */
! 39: chunk_t salt;
! 40:
! 41: /**
! 42: * Iterations for key derivation
! 43: */
! 44: uint64_t iterations;
! 45:
! 46: /**
! 47: * Encryption algorithm
! 48: */
! 49: encryption_algorithm_t encr;
! 50:
! 51: /**
! 52: * Encryption key length
! 53: */
! 54: size_t keylen;
! 55:
! 56: /**
! 57: * Crypter
! 58: */
! 59: crypter_t *crypter;
! 60:
! 61:
! 62: /**
! 63: * The encryption scheme
! 64: */
! 65: enum {
! 66: PKCS5_SCHEME_PBES1,
! 67: PKCS5_SCHEME_PBES2,
! 68: PKCS5_SCHEME_PKCS12,
! 69: } scheme;
! 70:
! 71: /**
! 72: * Data used for individual schemes
! 73: */
! 74: union {
! 75: struct {
! 76: /**
! 77: * Hash algorithm
! 78: */
! 79: hash_algorithm_t hash;
! 80:
! 81: /**
! 82: * Hasher
! 83: */
! 84: hasher_t *hasher;
! 85:
! 86: } pbes1;
! 87: struct {
! 88: /**
! 89: * PRF algorithm
! 90: */
! 91: pseudo_random_function_t prf_alg;
! 92:
! 93: /**
! 94: * PRF
! 95: */
! 96: prf_t * prf;
! 97:
! 98: /**
! 99: * IV
! 100: */
! 101: chunk_t iv;
! 102:
! 103: } pbes2;
! 104: } data;
! 105: };
! 106:
! 107: /**
! 108: * Verify padding of decrypted blob.
! 109: * Length of blob is adjusted accordingly.
! 110: */
! 111: static bool verify_padding(crypter_t *crypter, chunk_t *blob)
! 112: {
! 113: uint8_t padding, count;
! 114:
! 115: padding = count = blob->ptr[blob->len - 1];
! 116:
! 117: if (padding > crypter->get_block_size(crypter))
! 118: {
! 119: return FALSE;
! 120: }
! 121: for (; blob->len && count; --blob->len, --count)
! 122: {
! 123: if (blob->ptr[blob->len - 1] != padding)
! 124: {
! 125: return FALSE;
! 126: }
! 127: }
! 128: return TRUE;
! 129: }
! 130:
! 131: /**
! 132: * Prototype for key derivation functions.
! 133: */
! 134: typedef bool (*kdf_t)(private_pkcs5_t *this, chunk_t password, chunk_t key);
! 135:
! 136: /**
! 137: * Try to decrypt the given data with the given password using the given
! 138: * key derivation function. keymat is where the kdf function writes the key
! 139: * to, key and iv point to the actual keys and initialization vectors resp.
! 140: */
! 141: static bool decrypt_generic(private_pkcs5_t *this, chunk_t password,
! 142: chunk_t data, chunk_t *decrypted, kdf_t kdf,
! 143: chunk_t keymat, chunk_t key, chunk_t iv)
! 144: {
! 145: if (!kdf(this, password, keymat))
! 146: {
! 147: return FALSE;
! 148: }
! 149: if (!this->crypter->set_key(this->crypter, key) ||
! 150: !this->crypter->decrypt(this->crypter, data, iv, decrypted))
! 151: {
! 152: memwipe(keymat.ptr, keymat.len);
! 153: return FALSE;
! 154: }
! 155: memwipe(keymat.ptr, keymat.len);
! 156: if (verify_padding(this->crypter, decrypted))
! 157: {
! 158: return TRUE;
! 159: }
! 160: chunk_free(decrypted);
! 161: return FALSE;
! 162: }
! 163:
! 164: /**
! 165: * KDF as used by PKCS#12
! 166: */
! 167: static bool pkcs12_kdf(private_pkcs5_t *this, chunk_t password, chunk_t keymat)
! 168: {
! 169: chunk_t key, iv;
! 170:
! 171: key = chunk_create(keymat.ptr, this->keylen);
! 172: iv = chunk_create(keymat.ptr + this->keylen, keymat.len - this->keylen);
! 173:
! 174: return pkcs12_derive_key(this->data.pbes1.hash, password, this->salt,
! 175: this->iterations, PKCS12_KEY_ENCRYPTION, key) &&
! 176: pkcs12_derive_key(this->data.pbes1.hash, password, this->salt,
! 177: this->iterations, PKCS12_KEY_IV, iv);
! 178: }
! 179:
! 180: /**
! 181: * Function F of PBKDF2
! 182: */
! 183: static bool pbkdf2_f(chunk_t block, prf_t *prf, chunk_t seed,
! 184: uint64_t iterations)
! 185: {
! 186: chunk_t u;
! 187: uint64_t i;
! 188:
! 189: u = chunk_alloca(prf->get_block_size(prf));
! 190: if (!prf->get_bytes(prf, seed, u.ptr))
! 191: {
! 192: return FALSE;
! 193: }
! 194: memcpy(block.ptr, u.ptr, block.len);
! 195:
! 196: for (i = 1; i < iterations; i++)
! 197: {
! 198: if (!prf->get_bytes(prf, u, u.ptr))
! 199: {
! 200: return FALSE;
! 201: }
! 202: memxor(block.ptr, u.ptr, block.len);
! 203: }
! 204: return TRUE;
! 205: }
! 206:
! 207: /**
! 208: * PBKDF2 key derivation function for PBES2, key must be allocated
! 209: */
! 210: static bool pbkdf2(private_pkcs5_t *this, chunk_t password, chunk_t key)
! 211: {
! 212: prf_t *prf;
! 213: chunk_t keymat, block, seed;
! 214: size_t blocks;
! 215: uint32_t i = 0;
! 216:
! 217: prf = this->data.pbes2.prf;
! 218:
! 219: if (!prf->set_key(prf, password))
! 220: {
! 221: return FALSE;
! 222: }
! 223:
! 224: block.len = prf->get_block_size(prf);
! 225: blocks = (key.len - 1) / block.len + 1;
! 226: keymat = chunk_alloca(blocks * block.len);
! 227:
! 228: seed = chunk_cata("cc", this->salt, chunk_from_thing(i));
! 229:
! 230: for (; i < blocks; i++)
! 231: {
! 232: htoun32(seed.ptr + this->salt.len, i + 1);
! 233: block.ptr = keymat.ptr + (i * block.len);
! 234: if (!pbkdf2_f(block, prf, seed, this->iterations))
! 235: {
! 236: return FALSE;
! 237: }
! 238: }
! 239: memcpy(key.ptr, keymat.ptr, key.len);
! 240: return TRUE;
! 241: }
! 242:
! 243: /**
! 244: * PBKDF1 key derivation function for PBES1, key must be allocated
! 245: */
! 246: static bool pbkdf1(private_pkcs5_t *this, chunk_t password, chunk_t key)
! 247: {
! 248: hasher_t *hasher;
! 249: chunk_t hash;
! 250: uint64_t i;
! 251:
! 252: hasher = this->data.pbes1.hasher;
! 253:
! 254: hash = chunk_alloca(hasher->get_hash_size(hasher));
! 255: if (!hasher->get_hash(hasher, password, NULL) ||
! 256: !hasher->get_hash(hasher, this->salt, hash.ptr))
! 257: {
! 258: return FALSE;
! 259: }
! 260:
! 261: for (i = 1; i < this->iterations; i++)
! 262: {
! 263: if (!hasher->get_hash(hasher, hash, hash.ptr))
! 264: {
! 265: return FALSE;
! 266: }
! 267: }
! 268: memcpy(key.ptr, hash.ptr, key.len);
! 269: return TRUE;
! 270: }
! 271:
! 272: static bool ensure_crypto_primitives(private_pkcs5_t *this, chunk_t data)
! 273: {
! 274: if (!this->crypter)
! 275: {
! 276: this->crypter = lib->crypto->create_crypter(lib->crypto, this->encr,
! 277: this->keylen);
! 278: if (!this->crypter)
! 279: {
! 280: DBG1(DBG_ASN, " %N encryption algorithm not available",
! 281: encryption_algorithm_names, this->encr);
! 282: return FALSE;
! 283: }
! 284: }
! 285: if (data.len % this->crypter->get_block_size(this->crypter))
! 286: {
! 287: DBG1(DBG_ASN, " data size is not a multiple of block size");
! 288: return FALSE;
! 289: }
! 290: switch (this->scheme)
! 291: {
! 292: case PKCS5_SCHEME_PBES1:
! 293: {
! 294: if (!this->data.pbes1.hasher)
! 295: {
! 296: hasher_t *hasher;
! 297:
! 298: hasher = lib->crypto->create_hasher(lib->crypto,
! 299: this->data.pbes1.hash);
! 300: if (!hasher)
! 301: {
! 302: DBG1(DBG_ASN, " %N hash algorithm not available",
! 303: hash_algorithm_names, this->data.pbes1.hash);
! 304: return FALSE;
! 305: }
! 306: if (hasher->get_hash_size(hasher) < this->keylen)
! 307: {
! 308: hasher->destroy(hasher);
! 309: return FALSE;
! 310: }
! 311: this->data.pbes1.hasher = hasher;
! 312: }
! 313: break;
! 314: }
! 315: case PKCS5_SCHEME_PBES2:
! 316: {
! 317: if (!this->data.pbes2.prf)
! 318: {
! 319: prf_t *prf;
! 320:
! 321: prf = lib->crypto->create_prf(lib->crypto,
! 322: this->data.pbes2.prf_alg);
! 323: if (!prf)
! 324: {
! 325: DBG1(DBG_ASN, " %N prf algorithm not available",
! 326: pseudo_random_function_names,
! 327: this->data.pbes2.prf_alg);
! 328: return FALSE;
! 329: }
! 330: this->data.pbes2.prf = prf;
! 331: }
! 332: break;
! 333: }
! 334: case PKCS5_SCHEME_PKCS12:
! 335: break;
! 336: }
! 337: return TRUE;
! 338: }
! 339:
! 340: METHOD(pkcs5_t, decrypt, bool,
! 341: private_pkcs5_t *this, chunk_t password, chunk_t data, chunk_t *decrypted)
! 342: {
! 343: chunk_t keymat, key, iv;
! 344: kdf_t kdf;
! 345:
! 346: if (!ensure_crypto_primitives(this, data) || !decrypted)
! 347: {
! 348: return FALSE;
! 349: }
! 350: kdf = pbkdf1;
! 351: switch (this->scheme)
! 352: {
! 353: case PKCS5_SCHEME_PKCS12:
! 354: kdf = pkcs12_kdf;
! 355: /* fall-through */
! 356: case PKCS5_SCHEME_PBES1:
! 357: keymat = chunk_alloca(this->keylen +
! 358: this->crypter->get_iv_size(this->crypter));
! 359: key = chunk_create(keymat.ptr, this->keylen);
! 360: iv = chunk_create(keymat.ptr + this->keylen,
! 361: keymat.len - this->keylen);
! 362: break;
! 363: case PKCS5_SCHEME_PBES2:
! 364: kdf = pbkdf2;
! 365: keymat = chunk_alloca(this->keylen);
! 366: key = keymat;
! 367: iv = this->data.pbes2.iv;
! 368: break;
! 369: default:
! 370: return FALSE;
! 371: }
! 372: return decrypt_generic(this, password, data, decrypted, kdf,
! 373: keymat, key, iv);
! 374: }
! 375:
! 376: /**
! 377: * ASN.1 definition of a PBEParameter structure
! 378: */
! 379: static const asn1Object_t pbeParameterObjects[] = {
! 380: { 0, "PBEParameter", ASN1_SEQUENCE, ASN1_NONE }, /* 0 */
! 381: { 1, "salt", ASN1_OCTET_STRING, ASN1_BODY }, /* 1 */
! 382: { 1, "iterationCount", ASN1_INTEGER, ASN1_BODY }, /* 2 */
! 383: { 0, "exit", ASN1_EOC, ASN1_EXIT }
! 384: };
! 385: #define PBEPARAM_SALT 1
! 386: #define PBEPARAM_ITERATION_COUNT 2
! 387:
! 388: /**
! 389: * Parse a PBEParameter structure
! 390: */
! 391: static bool parse_pbes1_params(private_pkcs5_t *this, chunk_t blob, int level0)
! 392: {
! 393: asn1_parser_t *parser;
! 394: chunk_t object;
! 395: int objectID;
! 396: bool success;
! 397:
! 398: parser = asn1_parser_create(pbeParameterObjects, blob);
! 399: parser->set_top_level(parser, level0);
! 400:
! 401: while (parser->iterate(parser, &objectID, &object))
! 402: {
! 403: switch (objectID)
! 404: {
! 405: case PBEPARAM_SALT:
! 406: {
! 407: this->salt = chunk_clone(object);
! 408: break;
! 409: }
! 410: case PBEPARAM_ITERATION_COUNT:
! 411: {
! 412: this->iterations = asn1_parse_integer_uint64(object);
! 413: break;
! 414: }
! 415: }
! 416: }
! 417: success = parser->success(parser);
! 418: parser->destroy(parser);
! 419: return success;
! 420: }
! 421:
! 422: /**
! 423: * ASN.1 definition of a PBKDF2-params structure
! 424: * The salt is actually a CHOICE and could be an AlgorithmIdentifier from
! 425: * PBKDF2-SaltSources (but as per RFC 8018 that's for future versions).
! 426: * The PRF algorithm is actually defined as DEFAULT and not OPTIONAL, but the
! 427: * parser can't handle ASN1_DEF with SEQUENCEs.
! 428: */
! 429: static const asn1Object_t pbkdf2ParamsObjects[] = {
! 430: { 0, "PBKDF2-params", ASN1_SEQUENCE, ASN1_NONE }, /* 0 */
! 431: { 1, "salt", ASN1_OCTET_STRING, ASN1_BODY }, /* 1 */
! 432: { 1, "iterationCount",ASN1_INTEGER, ASN1_BODY }, /* 2 */
! 433: { 1, "keyLength", ASN1_INTEGER, ASN1_OPT|ASN1_BODY }, /* 3 */
! 434: { 1, "end opt", ASN1_EOC, ASN1_END }, /* 4 */
! 435: { 1, "prf", ASN1_SEQUENCE, ASN1_OPT|ASN1_RAW }, /* 5 */
! 436: { 1, "end opt", ASN1_EOC, ASN1_END }, /* 6 */
! 437: { 0, "exit", ASN1_EOC, ASN1_EXIT }
! 438: };
! 439: #define PBKDF2_SALT 1
! 440: #define PBKDF2_ITERATION_COUNT 2
! 441: #define PBKDF2_KEYLENGTH 3
! 442: #define PBKDF2_PRF 5
! 443:
! 444: /**
! 445: * Parse a PBKDF2-params structure
! 446: */
! 447: static bool parse_pbkdf2_params(private_pkcs5_t *this, chunk_t blob, int level0)
! 448: {
! 449: asn1_parser_t *parser;
! 450: chunk_t object;
! 451: int objectID;
! 452: bool success = FALSE;
! 453:
! 454: parser = asn1_parser_create(pbkdf2ParamsObjects, blob);
! 455: parser->set_top_level(parser, level0);
! 456:
! 457: /* keylen is optional */
! 458: this->keylen = 0;
! 459: /* defaults to id-hmacWithSHA1 */
! 460: this->data.pbes2.prf_alg = PRF_HMAC_SHA1;
! 461:
! 462: while (parser->iterate(parser, &objectID, &object))
! 463: {
! 464: switch (objectID)
! 465: {
! 466: case PBKDF2_SALT:
! 467: {
! 468: this->salt = chunk_clone(object);
! 469: break;
! 470: }
! 471: case PBKDF2_ITERATION_COUNT:
! 472: {
! 473: this->iterations = asn1_parse_integer_uint64(object);
! 474: break;
! 475: }
! 476: case PBKDF2_KEYLENGTH:
! 477: {
! 478: this->keylen = (size_t)asn1_parse_integer_uint64(object);
! 479: break;
! 480: }
! 481: case PBKDF2_PRF:
! 482: {
! 483: int oid;
! 484:
! 485: oid = asn1_parse_algorithmIdentifier(object,
! 486: parser->get_level(parser) + 1, NULL);
! 487: this->data.pbes2.prf_alg = pseudo_random_function_from_oid(oid);
! 488: if (this->data.pbes2.prf_alg == PRF_UNDEFINED)
! 489: { /* unsupported PRF algorithm */
! 490: goto end;
! 491: }
! 492: break;
! 493: }
! 494: }
! 495: }
! 496: success = parser->success(parser);
! 497: end:
! 498: parser->destroy(parser);
! 499: return success;
! 500: }
! 501:
! 502: /**
! 503: * ASN.1 definition of a PBES2-params structure
! 504: */
! 505: static const asn1Object_t pbes2ParamsObjects[] = {
! 506: { 0, "PBES2-params", ASN1_SEQUENCE, ASN1_NONE }, /* 0 */
! 507: { 1, "keyDerivationFunc", ASN1_EOC, ASN1_RAW }, /* 1 */
! 508: { 1, "encryptionScheme", ASN1_EOC, ASN1_RAW }, /* 2 */
! 509: { 0, "exit", ASN1_EOC, ASN1_EXIT }
! 510: };
! 511: #define PBES2PARAMS_KEY_DERIVATION_FUNC 1
! 512: #define PBES2PARAMS_ENCRYPTION_SCHEME 2
! 513:
! 514: /**
! 515: * Parse a PBES2-params structure
! 516: */
! 517: static bool parse_pbes2_params(private_pkcs5_t *this, chunk_t blob, int level0)
! 518: {
! 519: asn1_parser_t *parser;
! 520: chunk_t object, params;
! 521: size_t keylen;
! 522: int objectID;
! 523: bool success = FALSE;
! 524:
! 525: parser = asn1_parser_create(pbes2ParamsObjects, blob);
! 526: parser->set_top_level(parser, level0);
! 527:
! 528: while (parser->iterate(parser, &objectID, &object))
! 529: {
! 530: switch (objectID)
! 531: {
! 532: case PBES2PARAMS_KEY_DERIVATION_FUNC:
! 533: {
! 534: int oid = asn1_parse_algorithmIdentifier(object,
! 535: parser->get_level(parser) + 1, ¶ms);
! 536: if (oid != OID_PBKDF2)
! 537: { /* unsupported key derivation function */
! 538: goto end;
! 539: }
! 540: if (!parse_pbkdf2_params(this, params,
! 541: parser->get_level(parser) + 1))
! 542: {
! 543: goto end;
! 544: }
! 545: break;
! 546: }
! 547: case PBES2PARAMS_ENCRYPTION_SCHEME:
! 548: {
! 549: int oid = asn1_parse_algorithmIdentifier(object,
! 550: parser->get_level(parser) + 1, ¶ms);
! 551: this->encr = encryption_algorithm_from_oid(oid, &keylen);
! 552: if (this->encr == ENCR_UNDEFINED)
! 553: { /* unsupported encryption scheme */
! 554: goto end;
! 555: }
! 556: /* prefer encoded key length */
! 557: this->keylen = this->keylen ?: keylen / 8;
! 558: if (!this->keylen)
! 559: { /* set default key length for known algorithms */
! 560: switch (this->encr)
! 561: {
! 562: case ENCR_DES:
! 563: this->keylen = 8;
! 564: break;
! 565: case ENCR_3DES:
! 566: this->keylen = 24;
! 567: break;
! 568: case ENCR_BLOWFISH:
! 569: this->keylen = 16;
! 570: break;
! 571: default:
! 572: goto end;
! 573: }
! 574: }
! 575: if (!asn1_parse_simple_object(¶ms, ASN1_OCTET_STRING,
! 576: parser->get_level(parser) + 1, "IV"))
! 577: {
! 578: goto end;
! 579: }
! 580: this->data.pbes2.iv = chunk_clone(params);
! 581: break;
! 582: }
! 583: }
! 584: }
! 585: success = parser->success(parser);
! 586: end:
! 587: parser->destroy(parser);
! 588: return success;
! 589: }
! 590:
! 591: METHOD(pkcs5_t, destroy, void,
! 592: private_pkcs5_t *this)
! 593: {
! 594: DESTROY_IF(this->crypter);
! 595: chunk_free(&this->salt);
! 596: switch (this->scheme)
! 597: {
! 598: case PKCS5_SCHEME_PBES1:
! 599: DESTROY_IF(this->data.pbes1.hasher);
! 600: break;
! 601: case PKCS5_SCHEME_PBES2:
! 602: DESTROY_IF(this->data.pbes2.prf);
! 603: chunk_free(&this->data.pbes2.iv);
! 604: break;
! 605: case PKCS5_SCHEME_PKCS12:
! 606: break;
! 607: }
! 608: free(this);
! 609: }
! 610:
! 611: /*
! 612: * Described in header
! 613: */
! 614: pkcs5_t *pkcs5_from_algorithmIdentifier(chunk_t blob, int level0)
! 615: {
! 616: private_pkcs5_t *this;
! 617: chunk_t params;
! 618: int oid;
! 619:
! 620: INIT(this,
! 621: .public = {
! 622: .decrypt = _decrypt,
! 623: .destroy = _destroy,
! 624: },
! 625: .scheme = PKCS5_SCHEME_PBES1,
! 626: .keylen = 8,
! 627: );
! 628:
! 629: oid = asn1_parse_algorithmIdentifier(blob, level0, ¶ms);
! 630:
! 631: switch (oid)
! 632: {
! 633: case OID_PBE_MD5_DES_CBC:
! 634: this->encr = ENCR_DES;
! 635: this->data.pbes1.hash = HASH_MD5;
! 636: break;
! 637: case OID_PBE_SHA1_DES_CBC:
! 638: this->encr = ENCR_DES;
! 639: this->data.pbes1.hash = HASH_SHA1;
! 640: break;
! 641: case OID_PBE_SHA1_3DES_CBC:
! 642: this->scheme = PKCS5_SCHEME_PKCS12;
! 643: this->keylen = 24;
! 644: this->encr = ENCR_3DES;
! 645: this->data.pbes1.hash = HASH_SHA1;
! 646: break;
! 647: case OID_PBE_SHA1_RC2_CBC_40:
! 648: case OID_PBE_SHA1_RC2_CBC_128:
! 649: this->scheme = PKCS5_SCHEME_PKCS12;
! 650: this->keylen = (oid == OID_PBE_SHA1_RC2_CBC_40) ? 5 : 16;
! 651: this->encr = ENCR_RC2_CBC;
! 652: this->data.pbes1.hash = HASH_SHA1;
! 653: break;
! 654: case OID_PBES2:
! 655: this->scheme = PKCS5_SCHEME_PBES2;
! 656: break;
! 657: default:
! 658: /* encryption scheme not supported */
! 659: goto failure;
! 660: }
! 661:
! 662: switch (this->scheme)
! 663: {
! 664: case PKCS5_SCHEME_PBES1:
! 665: case PKCS5_SCHEME_PKCS12:
! 666: if (!parse_pbes1_params(this, params, level0))
! 667: {
! 668: goto failure;
! 669: }
! 670: break;
! 671: case PKCS5_SCHEME_PBES2:
! 672: if (!parse_pbes2_params(this, params, level0))
! 673: {
! 674: goto failure;
! 675: }
! 676: break;
! 677: }
! 678: return &this->public;
! 679:
! 680: failure:
! 681: destroy(this);
! 682: return NULL;
! 683: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>