Changeset 95656 in vbox for trunk/src/VBox/Runtime/common/crypto
- Timestamp:
- Jul 15, 2022 12:59:55 AM (3 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/VBox/Runtime/common/crypto/pkcs7-sign.cpp
r95624 r95656 43 43 # include "internal/iprt-openssl.h" 44 44 # include "internal/openssl-pre.h" 45 # include <openssl/asn1t.h> 45 46 # include <openssl/pkcs7.h> 46 47 # include <openssl/cms.h> … … 84 85 // 85 86 87 #ifdef IPRT_WITH_OPENSSL 88 89 static int rtCrPkcs7SimpleSignSignedDataDoV1TweakContent(PKCS7 *pOsslPkcs7, const char *pszContentId, 90 const void *pvData, size_t cbData, 91 PRTERRINFO pErrInfo) 92 { 93 AssertReturn(pszContentId, RTErrInfoSet(pErrInfo, VERR_CR_PKCS7_MISSING_CONTENT_TYPE_ATTRIB, 94 "RTCRPKCS7SIGN_SD_F_NO_DATA_ENCAP requires content type in additional attribs")); 95 96 /* 97 * Create a new inner PKCS#7 content container, forcing it to the 'other' type. 98 */ 99 PKCS7 *pOsslInnerContent = PKCS7_new(); 100 if (!pOsslInnerContent) 101 return RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "PKCS7_new failed"); 102 103 /* Set the type. */ 104 int rc; 105 pOsslInnerContent->type = OBJ_txt2obj(pszContentId, 1); 106 if (pOsslInnerContent->type) 107 { 108 /* Create a dynamic ASN1 type which we set to a sequence. */ 109 ASN1_TYPE *pOsslOther = pOsslInnerContent->d.other = ASN1_TYPE_new(); 110 if (pOsslOther) 111 { 112 pOsslOther->type = V_ASN1_SEQUENCE; 113 114 /* Create a string and put the data in it. */ 115 ASN1_STRING *pOsslStr = pOsslOther->value.sequence = ASN1_STRING_new(); 116 if (pOsslStr) 117 { 118 rc = ASN1_STRING_set(pOsslStr, pvData, (int)cbData); /* copies the buffer content */ 119 if (rc > 0) 120 { 121 /* 122 * Set the content in the PKCS#7 signed data we're constructing. 123 * This consumes pOsslInnerContent on success. 124 */ 125 rc = PKCS7_set_content(pOsslPkcs7, pOsslInnerContent); 126 if (rc > 0) 127 return VINF_SUCCESS; 128 129 /* failed */ 130 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "PKCS7_set_content"); 131 } 132 else 133 rc = RTErrInfoSetF(pErrInfo, VERR_NO_MEMORY, "ASN1_STRING_set(,,%#x)", cbData); 134 } 135 else 136 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "ASN1_STRING_new"); 137 } 138 else 139 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "ASN1_TYPE_new"); 140 } 141 else 142 rc = RTErrInfoSetF(pErrInfo, VERR_NO_MEMORY, "OBJ_txt2obj(%s, 1) failed", pszContentId); 143 PKCS7_free(pOsslInnerContent); 144 return rc; 145 } 146 147 static int rtCrPkcs7SimpleSignSignedDataDoV1AttribConversion(PKCS7_SIGNER_INFO *pSignerInfo, 148 PCRTCRPKCS7ATTRIBUTES pAdditionalAuthenticatedAttribs, 149 const char **ppszContentId, PRTERRINFO pErrInfo) 150 { 151 int rc = VINF_SUCCESS; 152 *ppszContentId = NULL; 153 154 if (pAdditionalAuthenticatedAttribs) 155 { 156 157 /* 158 * Convert each attribute. 159 */ 160 STACK_OF(X509_ATTRIBUTE) *pOsslAttributes = sk_X509_ATTRIBUTE_new_null(); 161 for (uint32_t i = 0; i < pAdditionalAuthenticatedAttribs->cItems && RT_SUCCESS(rc); i++) 162 { 163 PCRTCRPKCS7ATTRIBUTE pAttrib = pAdditionalAuthenticatedAttribs->papItems[i]; 164 165 /* Look out for content type, as we will probably need that for 166 RTCRPKCS7SIGN_SD_F_NO_DATA_ENCAP later. */ 167 if ( pAttrib->enmType == RTCRPKCS7ATTRIBUTETYPE_OBJ_IDS 168 && RTAsn1ObjId_CompareWithString(&pAttrib->Type, RTCR_PKCS9_ID_CONTENT_TYPE_OID) == 0) 169 { 170 AssertBreakStmt(!*ppszContentId, rc = VERR_CR_PKCS7_BAD_CONTENT_TYPE_ATTRIB); 171 AssertBreakStmt(pAttrib->uValues.pObjIds && pAttrib->uValues.pObjIds->cItems == 1, 172 rc = VERR_CR_PKCS7_BAD_CONTENT_TYPE_ATTRIB); 173 *ppszContentId = pAttrib->uValues.pObjIds->papItems[0]->szObjId; 174 } 175 176 /* The conversion (IPRT encode, OpenSSL decode). */ 177 X509_ATTRIBUTE *pOsslAttrib; 178 rc = rtCrOpenSslConvertPkcs7Attribute((void **)&pOsslAttrib, pAttrib, pErrInfo); 179 if (RT_SUCCESS(rc)) 180 { 181 if (!sk_X509_ATTRIBUTE_push(pOsslAttributes, pOsslAttrib)) 182 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "sk_X509_ATTRIBUTE_push failed"); 183 } 184 } 185 186 /* 187 * If we've successfully converted all the attributes, make a deep copy 188 * (waste of resource, but whatever) into the signer info we're working on. 189 */ 190 if (RT_SUCCESS(rc)) 191 { 192 rc = PKCS7_set_signed_attributes(pSignerInfo, pOsslAttributes); /* deep copy */ 193 if (rc <= 0) 194 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "PKCS7_set_signed_attributes failed"); 195 } 196 197 /* 198 * Free the attributes (they were copied). Cannot use X509_ATTRIBUTE_pop_free as 199 * the callback causes Visual C++ to complain about exceptions on the callback. 200 */ 201 for (int i = sk_X509_ATTRIBUTE_num(pOsslAttributes) - 1; i >= 0; i--) 202 X509_ATTRIBUTE_free(sk_X509_ATTRIBUTE_value(pOsslAttributes, i)); 203 sk_X509_ATTRIBUTE_free(pOsslAttributes); 204 } 205 return rc; 206 } 207 208 static int rtCrPkcs7SimpleSignSignedDataDoV1(uint32_t fFlags, X509 *pOsslSigner, EVP_PKEY *pEvpPrivateKey, 209 BIO *pOsslData, const EVP_MD *pEvpMd, STACK_OF(X509) *pOsslAdditionalCerts, 210 PCRTCRPKCS7ATTRIBUTES pAdditionalAuthenticatedAttribs, 211 const void *pvData, size_t cbData, 212 BIO **ppOsslResult, PRTERRINFO pErrInfo) 213 { 214 /* 215 * Use PKCS7_sign with PKCS7_PARTIAL to start a extended the signing process. 216 */ 217 /* Create a ContentInfo we can modify using CMS_sign w/ CMS_PARTIAL. */ 218 unsigned int fOsslSign = PKCS7_BINARY | PKCS7_PARTIAL; 219 if (fFlags & RTCRPKCS7SIGN_SD_F_DEATCHED) 220 fOsslSign |= PKCS7_DETACHED; 221 if (fFlags & RTCRPKCS7SIGN_SD_F_NO_SMIME_CAP) 222 fOsslSign |= PKCS7_NOSMIMECAP; 223 int rc = VINF_SUCCESS; 224 PKCS7 *pCms = PKCS7_sign(NULL, NULL, pOsslAdditionalCerts, NULL, fOsslSign); 225 if (pCms != NULL) 226 { 227 /* 228 * Add a signer. 229 */ 230 PKCS7_SIGNER_INFO *pSignerInfo = PKCS7_sign_add_signer(pCms, pOsslSigner, pEvpPrivateKey, pEvpMd, fOsslSign); 231 if (pSignerInfo) 232 { 233 /* 234 * Add additional attributes to the signer. 235 */ 236 const char *pszContentId = NULL; 237 rc = rtCrPkcs7SimpleSignSignedDataDoV1AttribConversion(pSignerInfo, pAdditionalAuthenticatedAttribs, 238 &pszContentId, pErrInfo); 239 if (RT_SUCCESS(rc)) 240 { 241 /* 242 * Finalized and actually sign the data. 243 */ 244 rc = PKCS7_final(pCms, pOsslData, fOsslSign); 245 if (rc > 0) 246 { 247 /* 248 * Do content type/enclosure tweaking if requested. 249 */ 250 rc = VINF_SUCCESS; 251 if ( (fFlags & (RTCRPKCS7SIGN_SD_F_DEATCHED | RTCRPKCS7SIGN_SD_F_NO_DATA_ENCAP)) 252 == RTCRPKCS7SIGN_SD_F_NO_DATA_ENCAP) /** @todo maybe we want to also do this when the content type isn't 'data'. */ 253 rc = rtCrPkcs7SimpleSignSignedDataDoV1TweakContent(pCms, pszContentId, pvData, cbData, pErrInfo); 254 else 255 { 256 /** @todo Set content type if needed? */ 257 AssertMsg(!pszContentId || strcmp(pszContentId, RTCR_PKCS7_DATA_OID) == 0, 258 ("pszContentId=%s\n", pszContentId)); 259 rc = VINF_SUCCESS; 260 } 261 if (RT_SUCCESS(rc)) 262 { 263 /* 264 * Get the output and copy it into the result buffer. 265 */ 266 BIO *pOsslResult = BIO_new(BIO_s_mem()); 267 if (pOsslResult) 268 { 269 rc = i2d_PKCS7_bio(pOsslResult, pCms); 270 if (rc > 0) 271 { 272 *ppOsslResult = pOsslResult; 273 rc = VINF_SUCCESS; 274 } 275 else 276 { 277 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "i2d_CMS_bio"); 278 BIO_free(pOsslResult); 279 } 280 } 281 else 282 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "BIO_new/BIO_s_mem"); 283 } 284 } 285 else 286 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_final"); 287 } 288 else 289 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_add1_signer"); 290 } 291 PKCS7_free(pCms); 292 } 293 else 294 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_sign"); 295 return rc; 296 } 297 298 299 static int rtCrPkcs7SimpleSignSignedDataDoDefault(uint32_t fFlags, X509 *pOsslSigner, EVP_PKEY *pEvpPrivateKey, 300 BIO *pOsslData, const EVP_MD *pEvpMd, STACK_OF(X509) *pOsslAdditionalCerts, 301 PCRTCRPKCS7ATTRIBUTES pAdditionalAuthenticatedAttribs, 302 BIO **ppOsslResult, PRTERRINFO pErrInfo) 303 304 { 305 /* 306 * Use CMS_sign with CMS_PARTIAL to start a extended the signing process. 307 */ 308 /* Create a ContentInfo we can modify using CMS_sign w/ CMS_PARTIAL. */ 309 unsigned int fOsslSign = CMS_BINARY | CMS_PARTIAL; 310 if (fFlags & RTCRPKCS7SIGN_SD_F_DEATCHED) 311 fOsslSign |= CMS_DETACHED; 312 if (fFlags & RTCRPKCS7SIGN_SD_F_NO_SMIME_CAP) 313 fOsslSign |= CMS_NOSMIMECAP; 314 int rc = VINF_SUCCESS; 315 CMS_ContentInfo *pCms = CMS_sign(NULL, NULL, pOsslAdditionalCerts, NULL, fOsslSign); 316 if (pCms != NULL) 317 { 318 /* 319 * Set encapsulated content type if present in the auth attribs. 320 */ 321 uint32_t iAuthAttrSkip = UINT32_MAX; 322 for (uint32_t i = 0; i < pAdditionalAuthenticatedAttribs->cItems && RT_SUCCESS(rc); i++) 323 { 324 PCRTCRPKCS7ATTRIBUTE pAttrib = pAdditionalAuthenticatedAttribs->papItems[i]; 325 if ( pAttrib->enmType == RTCRPKCS7ATTRIBUTETYPE_OBJ_IDS 326 && RTAsn1ObjId_CompareWithString(&pAttrib->Type, RTCR_PKCS9_ID_CONTENT_TYPE_OID) == 0) 327 { 328 AssertBreakStmt(pAttrib->uValues.pObjIds && pAttrib->uValues.pObjIds->cItems == 1, 329 rc = VERR_INTERNAL_ERROR_3); 330 PCRTASN1OBJID pObjId = pAttrib->uValues.pObjIds->papItems[0]; 331 ASN1_OBJECT *pOsslObjId = OBJ_txt2obj(pObjId->szObjId, 0 /*no_name*/); 332 if (pOsslObjId) 333 { 334 rc = CMS_set1_eContentType(pCms, pOsslObjId); 335 ASN1_OBJECT_free(pOsslObjId); 336 if (rc < 0) 337 rc = RTErrInfoSetF(pErrInfo, VERR_CR_PKIX_GENERIC_ERROR, 338 "CMS_set1_eContentType(%s)", pObjId->szObjId); 339 } 340 else 341 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "OBJ_txt2obj"); 342 343 iAuthAttrSkip = i; 344 break; 345 } 346 } 347 if (RT_SUCCESS(rc)) 348 { 349 /* 350 * Add a signer. 351 */ 352 CMS_SignerInfo *pSignerInfo = CMS_add1_signer(pCms, pOsslSigner, pEvpPrivateKey, pEvpMd, fOsslSign); 353 if (pSignerInfo) 354 { 355 /* 356 * Add additional attributes, skipping the content type if found above. 357 */ 358 if (pAdditionalAuthenticatedAttribs) 359 for (uint32_t i = 0; i < pAdditionalAuthenticatedAttribs->cItems && RT_SUCCESS(rc); i++) 360 if (i != iAuthAttrSkip) 361 { 362 PCRTCRPKCS7ATTRIBUTE pAttrib = pAdditionalAuthenticatedAttribs->papItems[i]; 363 X509_ATTRIBUTE *pOsslAttrib; 364 rc = rtCrOpenSslConvertPkcs7Attribute((void **)&pOsslAttrib, pAttrib, pErrInfo); 365 if (RT_SUCCESS(rc)) 366 { 367 rc = CMS_signed_add1_attr(pSignerInfo, pOsslAttrib); 368 rtCrOpenSslFreeConvertedPkcs7Attribute((void **)pOsslAttrib); 369 if (rc <= 0) 370 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "CMS_signed_add1_attr"); 371 } 372 } 373 if (RT_SUCCESS(rc)) 374 { 375 /* 376 * Finalized and actually sign the data. 377 */ 378 rc = CMS_final(pCms, pOsslData, NULL /*dcont*/, fOsslSign); 379 if (rc > 0) 380 { 381 /* 382 * Get the output and copy it into the result buffer. 383 */ 384 BIO *pOsslResult = BIO_new(BIO_s_mem()); 385 if (pOsslResult) 386 { 387 rc = i2d_CMS_bio(pOsslResult, pCms); 388 if (rc > 0) 389 { 390 *ppOsslResult = pOsslResult; 391 rc = VINF_SUCCESS; 392 } 393 else 394 { 395 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "i2d_CMS_bio"); 396 BIO_free(pOsslResult); 397 } 398 } 399 else 400 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "BIO_new/BIO_s_mem"); 401 } 402 else 403 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_final"); 404 } 405 } 406 else 407 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_add1_signer"); 408 } 409 CMS_ContentInfo_free(pCms); 410 } 411 else 412 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_sign"); 413 return rc; 414 } 415 416 #endif /* IPRT_WITH_OPENSSL */ 417 86 418 87 419 … … 136 468 { 137 469 /* 138 * Use CMS_sign with CMS_PARTIAL to start a extended the signing process.470 * Do the work. 139 471 */ 140 /* Create a ContentInfo we can modify using CMS_sign w/ CMS_PARTIAL. */ 141 unsigned int fOsslSign = CMS_BINARY | CMS_PARTIAL; 142 if (fFlags & RTCRPKCS7SIGN_SD_F_DEATCHED) 143 fOsslSign |= CMS_DETACHED; 144 if (fFlags & RTCRPKCS7SIGN_SD_F_NO_SMIME_CAP) 145 fOsslSign |= CMS_NOSMIMECAP; 146 CMS_ContentInfo *pCms = CMS_sign(NULL, NULL, pOsslAdditionalCerts, NULL, fOsslSign); 147 if (pCms != NULL) 472 BIO *pOsslResult = NULL; 473 if (!(fFlags & RTCRPKCS7SIGN_SD_F_USE_V1)) 474 rc = rtCrPkcs7SimpleSignSignedDataDoDefault(fFlags, pOsslSigner, pEvpPrivateKey, pOsslData, pEvpMd, 475 pOsslAdditionalCerts, pAdditionalAuthenticatedAttribs, 476 &pOsslResult, pErrInfo); 477 else 478 rc = rtCrPkcs7SimpleSignSignedDataDoV1(fFlags, pOsslSigner, pEvpPrivateKey, pOsslData, pEvpMd, 479 pOsslAdditionalCerts, pAdditionalAuthenticatedAttribs, 480 pvData, cbData, 481 &pOsslResult, pErrInfo); 482 BIO_free(pOsslData); 483 if (RT_SUCCESS(rc)) 148 484 { 149 485 /* 150 * Set encapsulated content type if present in the auth attribs.486 * Copy out the result. 151 487 */ 152 uint32_t iAuthAttrSkip = UINT32_MAX; 153 for (uint32_t i = 0; i < pAdditionalAuthenticatedAttribs->cItems && RT_SUCCESS(rc); i++) 488 BUF_MEM *pBuf = NULL; 489 rc = (int)BIO_get_mem_ptr(pOsslResult, &pBuf); 490 if (rc > 0) 154 491 { 155 PCRTCRPKCS7ATTRIBUTE pAttrib = pAdditionalAuthenticatedAttribs->papItems[i]; 156 if ( pAttrib->enmType == RTCRPKCS7ATTRIBUTETYPE_OBJ_IDS 157 && RTAsn1ObjId_CompareWithString(&pAttrib->Type, RTCR_PKCS9_ID_CONTENT_TYPE_OID) == 0) 492 AssertPtr(pBuf); 493 size_t const cbResult = pBuf->length; 494 if ( cbResultBuf >= cbResult 495 && pvResult != NULL) 158 496 { 159 AssertBreakStmt(pAttrib->uValues.pObjIds && pAttrib->uValues.pObjIds->cItems == 1, 160 rc = VERR_INTERNAL_ERROR_3); 161 PCRTASN1OBJID pObjId = pAttrib->uValues.pObjIds->papItems[0]; 162 ASN1_OBJECT *pOsslObjId = OBJ_txt2obj(pObjId->szObjId, 0 /*no_name*/); 163 if (pOsslObjId) 164 { 165 rc = CMS_set1_eContentType(pCms, pOsslObjId); 166 ASN1_OBJECT_free(pOsslObjId); 167 if (rc < 0) 168 rc = RTErrInfoSetF(pErrInfo, VERR_CR_PKIX_GENERIC_ERROR, 169 "CMS_set1_eContentType(%s)", pObjId->szObjId); 170 } 171 else 172 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "OBJ_txt2obj"); 173 174 iAuthAttrSkip = i; 175 break; 176 } 177 } 178 if (RT_SUCCESS(rc)) 179 { 180 /* 181 * Add a signer. 182 */ 183 CMS_SignerInfo *pSignerInfo = CMS_add1_signer(pCms, pOsslSigner, pEvpPrivateKey, pEvpMd, fOsslSign); 184 if (pSignerInfo) 185 { 186 /* 187 * Add additional attributes, skipping the content type if found above. 188 */ 189 if (pAdditionalAuthenticatedAttribs) 190 for (uint32_t i = 0; i < pAdditionalAuthenticatedAttribs->cItems && RT_SUCCESS(rc); i++) 191 if (i != iAuthAttrSkip) 192 { 193 PCRTCRPKCS7ATTRIBUTE pAttrib = pAdditionalAuthenticatedAttribs->papItems[i]; 194 X509_ATTRIBUTE *pOsslAttrib; 195 rc = rtCrOpenSslConvertPkcs7Attribute((void **)&pOsslAttrib, pAttrib, pErrInfo); 196 if (RT_SUCCESS(rc)) 197 { 198 rc = CMS_signed_add1_attr(pSignerInfo, pOsslAttrib); 199 rtCrOpenSslFreeConvertedPkcs7Attribute((void **)pOsslAttrib); 200 if (rc <= 0) 201 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "CMS_signed_add1_attr"); 202 } 203 } 204 if (RT_SUCCESS(rc)) 205 { 206 /* 207 * Finalized and actually sign the data. 208 */ 209 rc = CMS_final(pCms, pOsslData, NULL /*dcont*/, fOsslSign); 210 if (rc > 0) 211 { 212 /* 213 * Get the output and copy it into the result buffer. 214 */ 215 BIO *pOsslResult = BIO_new(BIO_s_mem()); 216 if (pOsslResult) 217 { 218 rc = i2d_CMS_bio(pOsslResult, pCms); 219 if (rc > 0) 220 { 221 BUF_MEM *pBuf = NULL; 222 rc = (int)BIO_get_mem_ptr(pOsslResult, &pBuf); 223 if (rc > 0) 224 { 225 AssertPtr(pBuf); 226 size_t const cbResult = pBuf->length; 227 if ( cbResultBuf >= cbResult 228 && pvResult != NULL) 229 { 230 memcpy(pvResult, pBuf->data, cbResult); 231 rc = VINF_SUCCESS; 232 } 233 else 234 rc = VERR_BUFFER_OVERFLOW; 235 *pcbResult = cbResult; 236 } 237 else 238 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "BIO_get_mem_ptr"); 239 } 240 else 241 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "i2d_CMS_bio"); 242 BIO_free(pOsslResult); 243 } 244 else 245 rc = RTErrInfoSet(pErrInfo, VERR_NO_MEMORY, "BIO_new/BIO_s_mem"); 246 } 247 else 248 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_final"); 249 } 497 memcpy(pvResult, pBuf->data, cbResult); 498 rc = VINF_SUCCESS; 250 499 } 251 500 else 252 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_add1_signer"); 501 rc = VERR_BUFFER_OVERFLOW; 502 *pcbResult = cbResult; 253 503 } 254 CMS_ContentInfo_free(pCms); 504 else 505 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "BIO_get_mem_ptr"); 506 BIO_free(pOsslResult); 255 507 } 256 else257 rc = RTErrInfoSet(pErrInfo, VERR_GENERAL_FAILURE, "CMS_sign");258 BIO_free(pOsslData);259 508 } 260 509 }
Note:
See TracChangeset
for help on using the changeset viewer.