From 45d6379135504814ab723b57f0eb8be23393a51d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 09:24:22 +0200 Subject: Adding upstream version 1:9.16.44. Signed-off-by: Daniel Baumann --- lib/dns/tests/keytable_test.c | 720 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 720 insertions(+) create mode 100644 lib/dns/tests/keytable_test.c (limited to 'lib/dns/tests/keytable_test.c') diff --git a/lib/dns/tests/keytable_test.c b/lib/dns/tests/keytable_test.c new file mode 100644 index 0000000..670b1b2 --- /dev/null +++ b/lib/dns/tests/keytable_test.c @@ -0,0 +1,720 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#if HAVE_CMOCKA + +#include +#include /* IWYU pragma: keep */ +#include +#include +#include +#include +#include +#include +#include + +#define UNIT_TESTING +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "dnstest.h" + +static int +_setup(void **state) { + isc_result_t result; + + UNUSED(state); + + result = dns_test_begin(NULL, true); + assert_int_equal(result, ISC_R_SUCCESS); + + return (0); +} + +static int +_teardown(void **state) { + UNUSED(state); + + dns_test_end(); + + return (0); +} + +dns_keytable_t *keytable = NULL; +dns_ntatable_t *ntatable = NULL; + +static const char *keystr1 = "BQEAAAABok+vaUC9neRv8yeT/" + "FEGgN7svR8s7VBUVSBd8NsAiV8AlaAg " + "O5FHar3JQd95i/puZos6Vi6at9/" + "JBbN8qVmO2AuiXxVqfxMKxIcy+LEB " + "0Vw4NaSJ3N3uaVREso6aTSs98H/" + "25MjcwLOr7SFfXA7bGhZatLtYY/xu kp6Km5hMfkE="; + +static const char *keystr2 = "BQEAAAABwuHz9Cem0BJ0JQTO7C/a3McR6hMaufljs1dfG/" + "inaJpYv7vH " + "XTrAOm/MeKp+/x6eT4QLru0KoZkvZJnqTI8JyaFTw2OM/" + "ItBfh/hL2lm " + "Cft2O7n3MfeqYtvjPnY7dWghYW4sVfH7VVEGm958o9nfi7953" + "2Qeklxh x8pXWdeAaRU="; + +static dns_view_t *view = NULL; + +/* + * Test utilities. In general, these assume input parameters are valid + * (checking with assert_int_equal, thus aborting if not) and unlikely run time + * errors (such as memory allocation failure) won't happen. This helps keep + * the test code concise. + */ + +/* + * Utility to convert C-string to dns_name_t. Return a pointer to + * static data, and so is not thread safe. + */ +static dns_name_t * +str2name(const char *namestr) { + static dns_fixedname_t fname; + static dns_name_t *name; + static isc_buffer_t namebuf; + void *deconst_namestr; + + name = dns_fixedname_initname(&fname); + DE_CONST(namestr, deconst_namestr); /* OK, since we don't modify it */ + isc_buffer_init(&namebuf, deconst_namestr, strlen(deconst_namestr)); + isc_buffer_add(&namebuf, strlen(namestr)); + assert_int_equal( + dns_name_fromtext(name, &namebuf, dns_rootname, 0, NULL), + ISC_R_SUCCESS); + + return (name); +} + +static void +create_keystruct(uint16_t flags, uint8_t proto, uint8_t alg, const char *keystr, + dns_rdata_dnskey_t *keystruct) { + unsigned char keydata[4096]; + isc_buffer_t keydatabuf; + isc_region_t r; + const dns_rdataclass_t rdclass = dns_rdataclass_in; + + keystruct->common.rdclass = rdclass; + keystruct->common.rdtype = dns_rdatatype_dnskey; + keystruct->mctx = dt_mctx; + ISC_LINK_INIT(&keystruct->common, link); + keystruct->flags = flags; + keystruct->protocol = proto; + keystruct->algorithm = alg; + + isc_buffer_init(&keydatabuf, keydata, sizeof(keydata)); + assert_int_equal(isc_base64_decodestring(keystr, &keydatabuf), + ISC_R_SUCCESS); + isc_buffer_usedregion(&keydatabuf, &r); + keystruct->datalen = r.length; + keystruct->data = isc_mem_allocate(dt_mctx, r.length); + memmove(keystruct->data, r.base, r.length); +} + +static void +create_dsstruct(dns_name_t *name, uint16_t flags, uint8_t proto, uint8_t alg, + const char *keystr, unsigned char *digest, + dns_rdata_ds_t *dsstruct) { + isc_result_t result; + unsigned char rrdata[4096]; + isc_buffer_t rrdatabuf; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_dnskey_t dnskey; + + /* + * Populate DNSKEY rdata structure. + */ + create_keystruct(flags, proto, alg, keystr, &dnskey); + + /* + * Convert to wire format. + */ + isc_buffer_init(&rrdatabuf, rrdata, sizeof(rrdata)); + result = dns_rdata_fromstruct(&rdata, dnskey.common.rdclass, + dnskey.common.rdtype, &dnskey, + &rrdatabuf); + assert_int_equal(result, ISC_R_SUCCESS); + + /* + * Build DS rdata struct. + */ + result = dns_ds_fromkeyrdata(name, &rdata, DNS_DSDIGEST_SHA256, digest, + dsstruct); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_rdata_freestruct(&dnskey); +} + +/* Common setup: create a keytable and ntatable to test with a few keys */ +static void +create_tables() { + isc_result_t result; + unsigned char digest[ISC_MAX_MD_SIZE]; + dns_rdata_ds_t ds; + dns_fixedname_t fn; + dns_name_t *keyname = dns_fixedname_name(&fn); + isc_stdtime_t now; + + result = dns_test_makeview("view", &view); + assert_int_equal(result, ISC_R_SUCCESS); + + assert_int_equal(dns_keytable_create(dt_mctx, &keytable), + ISC_R_SUCCESS); + assert_int_equal( + dns_ntatable_create(view, taskmgr, timermgr, &ntatable), + ISC_R_SUCCESS); + + /* Add a normal key */ + dns_test_namefromstring("example.com", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr1, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, false, false, keyname, &ds), + ISC_R_SUCCESS); + + /* Add an initializing managed key */ + dns_test_namefromstring("managed.com", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr1, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, true, true, keyname, &ds), + ISC_R_SUCCESS); + + /* Add a null key */ + assert_int_equal(dns_keytable_marksecure(keytable, str2name("null." + "example")), + ISC_R_SUCCESS); + + /* Add a negative trust anchor, duration 1 hour */ + isc_stdtime_get(&now); + assert_int_equal(dns_ntatable_add(ntatable, + str2name("insecure.example"), false, + now, 3600), + ISC_R_SUCCESS); +} + +static void +destroy_tables() { + if (ntatable != NULL) { + dns_ntatable_detach(&ntatable); + } + if (keytable != NULL) { + dns_keytable_detach(&keytable); + } + + dns_view_detach(&view); +} + +/* add keys to the keytable */ +static void +add_test(void **state) { + dns_keynode_t *keynode = NULL; + dns_keynode_t *null_keynode = NULL; + unsigned char digest[ISC_MAX_MD_SIZE]; + dns_rdata_ds_t ds; + dns_fixedname_t fn; + dns_name_t *keyname = dns_fixedname_name(&fn); + + UNUSED(state); + + create_tables(); + + /* + * Getting the keynode for the example.com key should succeed. + */ + assert_int_equal( + dns_keytable_find(keytable, str2name("example.com"), &keynode), + ISC_R_SUCCESS); + + /* + * Try to add the same key. This should have no effect but + * report success. + */ + dns_test_namefromstring("example.com", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr1, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, false, false, keyname, &ds), + ISC_R_SUCCESS); + dns_keytable_detachkeynode(keytable, &keynode); + assert_int_equal( + dns_keytable_find(keytable, str2name("example.com"), &keynode), + ISC_R_SUCCESS); + + /* Add another key (different keydata) */ + dns_keytable_detachkeynode(keytable, &keynode); + create_dsstruct(keyname, 257, 3, 5, keystr2, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, false, false, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("example.com"), &keynode), + ISC_R_SUCCESS); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Get the keynode for the managed.com key. Ensure the + * retrieved key is an initializing key, then mark it as trusted using + * dns_keynode_trust() and ensure the latter works as expected. + */ + assert_int_equal( + dns_keytable_find(keytable, str2name("managed.com"), &keynode), + ISC_R_SUCCESS); + assert_int_equal(dns_keynode_initial(keynode), true); + dns_keynode_trust(keynode); + assert_int_equal(dns_keynode_initial(keynode), false); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Add a different managed key for managed.com, marking it as an + * initializing key. Since there is already a trusted key at the + * node, the node should *not* be marked as initializing. + */ + dns_test_namefromstring("managed.com", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr2, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, true, true, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("managed.com"), &keynode), + ISC_R_SUCCESS); + assert_int_equal(dns_keynode_initial(keynode), false); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Add the same managed key again, but this time mark it as a + * non-initializing key. Ensure the previously added key is upgraded + * to a non-initializing key and make sure there are still two key + * nodes for managed.com, both containing non-initializing keys. + */ + assert_int_equal(dns_keytable_add(keytable, true, false, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("managed.com"), &keynode), + ISC_R_SUCCESS); + assert_int_equal(dns_keynode_initial(keynode), false); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Add a managed key at a new node, two.com, marking it as an + * initializing key. + */ + dns_test_namefromstring("two.com", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr1, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, true, true, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("two.com"), &keynode), + ISC_R_SUCCESS); + assert_int_equal(dns_keynode_initial(keynode), true); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Add a different managed key for two.com, marking it as a + * non-initializing key. Since there is already an iniitalizing + * trust anchor for two.com and we haven't run dns_keynode_trust(), + * the initialization status should not change. + */ + create_dsstruct(keyname, 257, 3, 5, keystr2, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, true, false, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("two.com"), &keynode), + ISC_R_SUCCESS); + assert_int_equal(dns_keynode_initial(keynode), true); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * Add a normal key to a name that has a null key. The null key node + * will be updated with the normal key. + */ + assert_int_equal(dns_keytable_find(keytable, str2name("null.example"), + &null_keynode), + ISC_R_SUCCESS); + dns_test_namefromstring("null.example", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr2, digest, &ds); + assert_int_equal(dns_keytable_add(keytable, false, false, keyname, &ds), + ISC_R_SUCCESS); + assert_int_equal( + dns_keytable_find(keytable, str2name("null.example"), &keynode), + ISC_R_SUCCESS); + assert_ptr_equal(keynode, null_keynode); /* should be the same node */ + dns_keytable_detachkeynode(keytable, &null_keynode); + + /* + * Try to add a null key to a name that already has a key. It's + * effectively no-op, so the same key node is still there. + * (Note: this and above checks confirm that if a name has a null key + * that's the only key for the name). + */ + assert_int_equal(dns_keytable_marksecure(keytable, str2name("null." + "example")), + ISC_R_SUCCESS); + assert_int_equal(dns_keytable_find(keytable, str2name("null.example"), + &null_keynode), + ISC_R_SUCCESS); + assert_ptr_equal(keynode, null_keynode); + dns_keytable_detachkeynode(keytable, &null_keynode); + + dns_keytable_detachkeynode(keytable, &keynode); + destroy_tables(); +} + +/* delete keys from the keytable */ +static void +delete_test(void **state) { + UNUSED(state); + + create_tables(); + + /* dns_keytable_delete requires exact match */ + assert_int_equal(dns_keytable_delete(keytable, str2name("example.org")), + ISC_R_NOTFOUND); + assert_int_equal(dns_keytable_delete(keytable, str2name("s.example." + "com")), + ISC_R_NOTFOUND); + assert_int_equal(dns_keytable_delete(keytable, str2name("example.com")), + ISC_R_SUCCESS); + + /* works also for nodes with a null key */ + assert_int_equal(dns_keytable_delete(keytable, str2name("null." + "example")), + ISC_R_SUCCESS); + + /* or a negative trust anchor */ + assert_int_equal(dns_ntatable_delete(ntatable, str2name("insecure." + "example")), + ISC_R_SUCCESS); + + destroy_tables(); +} + +/* delete key nodes from the keytable */ +static void +deletekey_test(void **state) { + dns_rdata_dnskey_t dnskey; + dns_fixedname_t fn; + dns_name_t *keyname = dns_fixedname_name(&fn); + + UNUSED(state); + + create_tables(); + + /* key name doesn't match */ + dns_test_namefromstring("example.org", &fn); + create_keystruct(257, 3, 5, keystr1, &dnskey); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + ISC_R_NOTFOUND); + dns_rdata_freestruct(&dnskey); + + /* subdomain match is the same as no match */ + dns_test_namefromstring("sub.example.org", &fn); + create_keystruct(257, 3, 5, keystr1, &dnskey); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + ISC_R_NOTFOUND); + dns_rdata_freestruct(&dnskey); + + /* name matches but key doesn't match (resulting in PARTIALMATCH) */ + dns_test_namefromstring("example.com", &fn); + create_keystruct(257, 3, 5, keystr2, &dnskey); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + DNS_R_PARTIALMATCH); + dns_rdata_freestruct(&dnskey); + + /* + * exact match: should return SUCCESS on the first try, then + * PARTIALMATCH on the second (because the name existed but + * not a matching key). + */ + create_keystruct(257, 3, 5, keystr1, &dnskey); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + ISC_R_SUCCESS); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + DNS_R_PARTIALMATCH); + + /* + * after deleting the node, any deletekey or delete attempt should + * result in NOTFOUND. + */ + assert_int_equal(dns_keytable_delete(keytable, keyname), ISC_R_SUCCESS); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + ISC_R_NOTFOUND); + dns_rdata_freestruct(&dnskey); + + /* + * A null key node for a name is not deleted when searched by key; + * it must be deleted by dns_keytable_delete() + */ + dns_test_namefromstring("null.example", &fn); + create_keystruct(257, 3, 5, keystr1, &dnskey); + assert_int_equal(dns_keytable_deletekey(keytable, keyname, &dnskey), + DNS_R_PARTIALMATCH); + assert_int_equal(dns_keytable_delete(keytable, keyname), ISC_R_SUCCESS); + dns_rdata_freestruct(&dnskey); + + destroy_tables(); +} + +/* check find-variant operations */ +static void +find_test(void **state) { + dns_keynode_t *keynode = NULL; + dns_fixedname_t fname; + dns_name_t *name; + + UNUSED(state); + + create_tables(); + + /* + * dns_keytable_find() requires exact name match. It matches node + * that has a null key, too. + */ + assert_int_equal( + dns_keytable_find(keytable, str2name("example.org"), &keynode), + ISC_R_NOTFOUND); + assert_int_equal(dns_keytable_find(keytable, + str2name("sub.example.com"), + &keynode), + ISC_R_NOTFOUND); + assert_int_equal( + dns_keytable_find(keytable, str2name("example.com"), &keynode), + ISC_R_SUCCESS); + dns_keytable_detachkeynode(keytable, &keynode); + assert_int_equal( + dns_keytable_find(keytable, str2name("null.example"), &keynode), + ISC_R_SUCCESS); + dns_keytable_detachkeynode(keytable, &keynode); + + /* + * dns_keytable_finddeepestmatch() allows partial match. Also match + * nodes with a null key. + */ + name = dns_fixedname_initname(&fname); + assert_int_equal(dns_keytable_finddeepestmatch( + keytable, str2name("example.com"), name), + ISC_R_SUCCESS); + assert_true(dns_name_equal(name, str2name("example.com"))); + assert_int_equal(dns_keytable_finddeepestmatch( + keytable, str2name("s.example.com"), name), + ISC_R_SUCCESS); + assert_true(dns_name_equal(name, str2name("example.com"))); + assert_int_equal(dns_keytable_finddeepestmatch( + keytable, str2name("example.org"), name), + ISC_R_NOTFOUND); + assert_int_equal(dns_keytable_finddeepestmatch( + keytable, str2name("null.example"), name), + ISC_R_SUCCESS); + assert_true(dns_name_equal(name, str2name("null.example"))); + + destroy_tables(); +} + +/* check issecuredomain() */ +static void +issecuredomain_test(void **state) { + bool issecure; + const char **n; + const char *names[] = { "example.com", "sub.example.com", + "null.example", "sub.null.example", NULL }; + + UNUSED(state); + create_tables(); + + /* + * Domains that are an exact or partial match of a key name are + * considered secure. It's the case even if the key is null + * (validation will then fail, but that's actually the intended effect + * of installing a null key). + */ + for (n = names; *n != NULL; n++) { + assert_int_equal(dns_keytable_issecuredomain(keytable, + str2name(*n), NULL, + &issecure), + ISC_R_SUCCESS); + assert_true(issecure); + } + + /* + * If the key table has no entry (not even a null one) for a domain or + * any of its ancestors, that domain is considered insecure. + */ + assert_int_equal(dns_keytable_issecuredomain(keytable, + str2name("example.org"), + NULL, &issecure), + ISC_R_SUCCESS); + assert_false(issecure); + + destroy_tables(); +} + +/* check dns_keytable_dump() */ +static void +dump_test(void **state) { + FILE *f = fopen("/dev/null", "w"); + + UNUSED(state); + + create_tables(); + + /* + * Right now, we only confirm the dump attempt doesn't cause disruption + * (so we don't check the dump content). + */ + assert_int_equal(dns_keytable_dump(keytable, f), ISC_R_SUCCESS); + fclose(f); + + destroy_tables(); +} + +/* check negative trust anchors */ +static void +nta_test(void **state) { + isc_result_t result; + bool issecure, covered; + dns_fixedname_t fn; + dns_name_t *keyname = dns_fixedname_name(&fn); + unsigned char digest[ISC_MAX_MD_SIZE]; + dns_rdata_ds_t ds; + dns_view_t *myview = NULL; + isc_stdtime_t now; + + UNUSED(state); + + result = dns_test_makeview("view", &myview); + assert_int_equal(result, ISC_R_SUCCESS); + + result = isc_task_create(taskmgr, 0, &myview->task); + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_view_initsecroots(myview, dt_mctx); + assert_int_equal(result, ISC_R_SUCCESS); + result = dns_view_getsecroots(myview, &keytable); + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_view_initntatable(myview, taskmgr, timermgr); + assert_int_equal(result, ISC_R_SUCCESS); + result = dns_view_getntatable(myview, &ntatable); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_test_namefromstring("example", &fn); + create_dsstruct(keyname, 257, 3, 5, keystr1, digest, &ds); + result = dns_keytable_add(keytable, false, false, keyname, &ds), + assert_int_equal(result, ISC_R_SUCCESS); + + isc_stdtime_get(&now); + result = dns_ntatable_add(ntatable, str2name("insecure.example"), false, + now, 1); + assert_int_equal(result, ISC_R_SUCCESS); + + /* Should be secure */ + result = dns_view_issecuredomain(myview, + str2name("test.secure.example"), now, + true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_false(covered); + assert_true(issecure); + + /* Should not be secure */ + result = dns_view_issecuredomain(myview, + str2name("test.insecure.example"), now, + true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_true(covered); + assert_false(issecure); + + /* NTA covered */ + covered = dns_view_ntacovers(myview, now, str2name("insecure.example"), + dns_rootname); + assert_true(covered); + + /* Not NTA covered */ + covered = dns_view_ntacovers(myview, now, str2name("secure.example"), + dns_rootname); + assert_false(covered); + + /* As of now + 2, the NTA should be clear */ + result = dns_view_issecuredomain(myview, + str2name("test.insecure.example"), + now + 2, true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_false(covered); + assert_true(issecure); + + /* Now check deletion */ + result = dns_view_issecuredomain(myview, str2name("test.new.example"), + now, true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_false(covered); + assert_true(issecure); + + result = dns_ntatable_add(ntatable, str2name("new.example"), false, now, + 3600); + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_view_issecuredomain(myview, str2name("test.new.example"), + now, true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_true(covered); + assert_false(issecure); + + result = dns_ntatable_delete(ntatable, str2name("new.example")); + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_view_issecuredomain(myview, str2name("test.new.example"), + now, true, &covered, &issecure); + assert_int_equal(result, ISC_R_SUCCESS); + assert_false(covered); + assert_true(issecure); + + /* Clean up */ + dns_ntatable_detach(&ntatable); + dns_keytable_detach(&keytable); + dns_view_detach(&myview); +} + +int +main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(add_test), + cmocka_unit_test(delete_test), + cmocka_unit_test(deletekey_test), + cmocka_unit_test(find_test), + cmocka_unit_test(issecuredomain_test), + cmocka_unit_test(dump_test), + cmocka_unit_test(nta_test), + }; + + return (cmocka_run_group_tests(tests, _setup, _teardown)); +} + +#else /* HAVE_CMOCKA */ + +#include + +int +main(void) { + printf("1..0 # Skipped: cmocka not available\n"); + return (SKIPPED_TEST_EXIT_CODE); +} + +#endif /* if HAVE_CMOCKA */ -- cgit v1.2.3