From 11d3a83b04786c74fdbbbcdc99074c75666bd722 Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Thu, 24 Jul 2025 11:17:28 +0200 Subject: gpg: Add a notation with version information to signatures. * common/compliance.c (gnupg_manu_notation_value): New. * g10/build-packet.c (name_value_to_notation): New. * g10/options.h (COMPAT_NO_MANU): New. (LIST_SHOW_HIDDEN_NOTATIONS): New. (VERIFY_SHOW_HIDDEN_NOTATIONS): New. * g10/gpg.c (compatibility_flags): Add "no-manu". (parse_list_options): Add "show-hidden-notations". (main): Ditto for verify-options. * g10/import.c (list_standalone_revocation): Implement new list option. * g10/mainproc.c (check_sig_and_print):Ditto * g10/keyedit.c (keyedit_print_one_sig): Ditto. * g10/keylist.c (list_signature_print): Ditto. (show_notation): Handle hidden notation. * sm/keylist.c (oidtranstbl): Add OID for manu. * g10/sign.c (mk_notation_policy_etc): Add arg with_manu and insert extra notation. (write_signature_packets): Request manu notation in certain cases. (make_keysig_packet): Ditto. -- See DETAILS for a description of the format. Obviously this leaks the version of the used GnuPG version (major and minor only) and the fact that it was generated by GnuPG. However, by close inspection of the order of packets and and other meta data similar information can be found. We include this information to help the support desk to figure out problems. Sometimes users have very very old versions and the help desk needs to employ core developers to figure the cause. Having this information may eventually allow for better user support. This feature can be disabled unless certain compliance modes are enabled. --- common/compliance.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ common/compliance.h | 1 + doc/DETAILS | 50 ++++++++++++++++++++++++++++++++++--- doc/gpg.texi | 16 +++++++++--- g10/build-packet.c | 35 +++++++++++++++++++------- g10/gpg.c | 7 +++++- g10/import.c | 4 ++- g10/keyedit.c | 4 ++- g10/keylist.c | 14 +++++++---- g10/mainproc.c | 4 ++- g10/options.h | 3 +++ g10/packet.h | 1 + g10/sign.c | 54 ++++++++++++++++++++++++++++++++++------ sm/keylist.c | 1 + 14 files changed, 234 insertions(+), 31 deletions(-) diff --git a/common/compliance.c b/common/compliance.c index 6c271c199..db17e4aba 100644 --- a/common/compliance.c +++ b/common/compliance.c @@ -751,6 +751,77 @@ gnupg_status_compliance_flag (enum gnupg_compliance_mode compliance) } + +/* This function returns the value for the "manu" LibrePGP/rfc4880bis + * notation. See doc/DETAILS for a description. This value is also + * used for the manuNotation in X.509/CMS. */ +const char * +gnupg_manu_notation_value (enum gnupg_compliance_mode compliance) +{ + static char buffer[48]; /* Empty string indicates not yet initialized */ + static char buffer2[40]; + + if (!*buffer) + { + char *buf; + const char *s; + int n; + const char *fields[4]; + const char *vers1, *vers2; + int vers1len, vers2len; + int arch_id, os_id; + + arch_id = 0; + buf = gcry_get_config (0, "cpu-arch"); + if (buf && (n=split_fields_colon (buf, fields, DIM (fields))) >= 2) + { + if (!strcmp (fields[1], "x86") && n > 2) + { + if (!strcmp (fields[2], "amd64")) + arch_id = 2; + else if (!strcmp (fields[2], "i386")) + arch_id = 1; + } + else if (!strcmp (fields[1], "arm")) + arch_id = 3; + } + gcry_free (buf); +#ifdef HAVE_W32_SYSTEM + os_id = 1; +#elif defined(__linux__) + os_id = 2; +#elif defined (__unix__) || defined(__APPLE__) + os_id = 3; +#else + os_id = 0; +#endif + vers1 = PACKAGE_VERSION; + for (s=vers1, n=0; *s; s++) + if (*s=='.') + if (++n == 2) + break; + vers1len = s-vers1; + + vers2 = gcry_check_version (NULL); + for (s=vers2, n=0; *s; s++) + if (*s=='.') + if (++n == 2) + break; + vers2len = s-vers2; + + snprintf (buffer2, sizeof buffer2, "2,%.*s+%.*s,%d,%d", + vers1len, vers1, vers2len, vers2, arch_id, os_id); + snprintf (buffer, sizeof buffer, "%s,%d", + buffer2, get_assumed_de_vs_compliance ()? 2023 : 23); + } + + if (compliance == CO_DE_VS) + return buffer; + else + return buffer2; +} + + /* Parse the value of --compliance. Returns the value corresponding * to the given STRING according to OPTIONS of size LENGTH, or -1 * indicating that the lookup was unsuccessful, or the list of options diff --git a/common/compliance.h b/common/compliance.h index 111fdc74b..1ab39d607 100644 --- a/common/compliance.h +++ b/common/compliance.h @@ -78,6 +78,7 @@ int gnupg_gcrypt_is_compliant (enum gnupg_compliance_mode compliance); const char *gnupg_status_compliance_flag (enum gnupg_compliance_mode compliance); +const char *gnupg_manu_notation_value (enum gnupg_compliance_mode compliance); struct gnupg_compliance_option { diff --git a/doc/DETAILS b/doc/DETAILS index 0504c80bb..246c4227d 100644 --- a/doc/DETAILS +++ b/doc/DETAILS @@ -1666,6 +1666,7 @@ Status codes are: 1.3.6.1.4.1.11591.2 GnuPG 1.3.6.1.4.1.11591.2.1 notation 1.3.6.1.4.1.11591.2.1.1 pkaAddress + 1.3.6.1.4.1.11591.2.1.2 manuNotation (as IA5String) 1.3.6.1.4.1.11591.2.2 X.509 extensions 1.3.6.1.4.1.11591.2.2.1 standaloneCertificate 1.3.6.1.4.1.11591.2.2.2 wellKnownPrivateKey @@ -1774,7 +1775,6 @@ Description of some debug flags: - T6390 :: Notes on use of X25519 in GnuPG (https://dev.gnupg.org/T6390) - ** v3 fingerprints For packet version 3 we calculate the keyids this way: - RSA :: Low 64 bits of n @@ -1782,12 +1782,56 @@ Description of some debug flags: calculate a RMD160 hash value from it. This is used as the fingerprint and the low 64 bits are the keyid. -** gnupg.org notations +** Used notations + + - manu :: LibrePGP/rfc4880bis defined standard notation used by + GnuPG and other implementaions to convey additional + information about the implementation used to create + a key or signature. This is a list of comma delimited + values with these defined fields: + + | field | name | defined values | + |-------+------------------+------------------------| + | 1 | software product | see: prod-id | + | 2 | software version | e.g. "2.2", "2.5+1.12" | + | 3 | architecture | see: arch-id | + | 4 | operating system | see: os-id | + | 5 | compliance class | e.g. "23", "2023" | + + | prod-id | name | + |---------+-------------| + | 1 | PGP | + | 2 | GnuPG | + | 3 | Greenshield | + | 4 | RNP | + + | arch-id | cpu | + |---------+-------| + | 1 | i686 | + | 2 | amd64 | + | 3 | arm64 | + | 4 | riscv | + + | os-id | os | + |-------+---------| + | 1 | Windows | + | 2 | Linux | + | 3 | BSD | + + If a value for a field is not known, the empty string + may be used. The values are also used for the X.509/CMS + extension 1.3.6.1.4.1.11591.2.1.2. The compliance class + values are 23 for "de-vs" and 2023 for non-approved "de-vs". + + This notation shall be human readable. It is defined in + away to minimize its size but to be easily viewable by + standard software. - rem@gnupg.org :: Used by Kleopatra to implement the tag feature. These tags are used to mark keys for easier searching and grouping. - + - cpl@gnupg.org :: Used by GnuPG to mark the compliance of + encryption subkeys. ** Simplified revocation certificates Revocation certificates consist only of the signature packet; diff --git a/doc/gpg.texi b/doc/gpg.texi index 63e87e528..91bc73e8c 100644 --- a/doc/gpg.texi +++ b/doc/gpg.texi @@ -1420,11 +1420,15 @@ give the opposite meaning. The options are: @item show-notations @itemx show-std-notations @itemx show-user-notations + @itemx show-hidden-notations @opindex list-options:show-notations @opindex list-options:show-std-notations @opindex list-options:show-user-notations + @opindex list-options:show-hidden-notations Show all, IETF standard, or user-defined signature notations in the - @option{--check-signatures} listings. Defaults to no. + @option{--check-sigs} listings. Hidden notations are those which + are automatically inserted by an implementation and not worthy to + mention. Defaults to no. @item show-x509-notations @opindex list-options:show-x509-notations @@ -1513,11 +1517,15 @@ the opposite meaning. The options are: @item show-notations @itemx show-std-notations @itemx show-user-notations + @itemx show-hidden-notations @opindex verify-options:show-notations @opindex verify-options:show-std-notations @opindex verify-options:show-user-notations + @opindex verify-options:show-hidden-notations Show all, IETF standard, or user-defined signature notations in the - signature being verified. Defaults to IETF standard. + signature being verified. Hidden notations are those which are + automatically inserted by an implementation and not worthy to + mention. Defaults to IETF standard. @item show-keyserver-urls @opindex verify-options:show-keyserver-urls @@ -3374,7 +3382,9 @@ given once only the name of the program and the major number is emitted, given twice the minor is also emitted, given thrice the micro is added, and given four times an operating system identification is also emitted. @option{--no-emit-version} (default) disables the version -line. +line. Note that unless the @option{--compatibility-flags} have +a "no-manu" flag set, the GnuPG and Libgcrypt major and minor version +(e.g. "2.6+1.11") is included in signature packets and keys. @item --sig-notation @{@var{name}=@var{value}@} @itemx --cert-notation @{@var{name}=@var{value}@} diff --git a/g10/build-packet.c b/g10/build-packet.c index 57a67d9f4..0eb83463f 100644 --- a/g10/build-packet.c +++ b/g10/build-packet.c @@ -1577,17 +1577,18 @@ notation_value_to_human_readable_string (struct notation *notation) return xstrdup (notation->value); } -/* Turn the notation described by the string STRING into a notation. - - STRING has the form: - - -name - Delete the notation. - - name@domain.name=value - Normal notation - - !name@domain.name=value - Notation with critical bit set. - - The caller must free the result using free_notation(). */ +/* Turn the notation described by the string STRING into a notation. + * + * STRING has the form: + * + * - -name - Delete the notation. + * - name@domain.name=value - Normal notation + * - !name@domain.name=value - Notation with critical bit set. + * + * The caller must free the result using free_notation(). */ struct notation * -string_to_notation(const char *string,int is_utf8) +string_to_notation (const char *string, int is_utf8) { const char *s; int saw_at=0; @@ -1676,6 +1677,22 @@ string_to_notation(const char *string,int is_utf8) return NULL; } + +/* Turn the notation described by NAME and VALUE into a notation. + * This will be a human readble non-critical notation. + * The caller must free the result using free_notation(). */ +struct notation * +name_value_to_notation (const char *name, const char *value) +{ + struct notation *notation; + + notation = xcalloc (1, sizeof *notation); + notation->name = xstrdup (name); + notation->value = xstrdup (value); + return notation; +} + + /* Like string_to_notation, but store opaque data rather than human readable data. */ struct notation * diff --git a/g10/gpg.c b/g10/gpg.c index 296d5fceb..5cd546ba0 100644 --- a/g10/gpg.c +++ b/g10/gpg.c @@ -1059,6 +1059,7 @@ static struct compatibility_flags_s compatibility_flags [] = { COMPAT_PARALLELIZED, "parallelized" }, { COMPAT_T7014_OLD, "t7014-old" }, { COMPAT_COMPR_KEYS, "compr-keys" }, + { COMPAT_NO_MANU, "no-manu" }, { 0, NULL } }; @@ -2116,6 +2117,8 @@ parse_list_options(char *str) NULL}, {"show-user-notations",LIST_SHOW_USER_NOTATIONS,NULL, N_("show user-supplied notations during signature listings")}, + {"show-hidden-notations",LIST_SHOW_HIDDEN_NOTATIONS,NULL, + NULL}, {"show-x509-notations",LIST_SHOW_X509_NOTATIONS,NULL, NULL }, {"store-x509-notations",LIST_STORE_X509_NOTATIONS,NULL, NULL }, {"show-keyserver-urls",LIST_SHOW_KEYSERVER_URLS,NULL, @@ -3498,7 +3501,9 @@ main (int argc, char **argv) NULL}, {"show-user-notations",VERIFY_SHOW_USER_NOTATIONS,NULL, N_("show user-supplied notations during signature verification")}, - {"show-keyserver-urls",VERIFY_SHOW_KEYSERVER_URLS,NULL, + {"show-hidden-notations",VERIFY_SHOW_HIDDEN_NOTATIONS,NULL, + NULL}, + {"show-keyserver-urls",VERIFY_SHOW_KEYSERVER_URLS,NULL, N_("show preferred keyserver URLs during signature verification")}, {"show-uid-validity",VERIFY_SHOW_UID_VALIDITY,NULL, N_("show user ID validity during signature verification")}, diff --git a/g10/import.c b/g10/import.c index 5985d177b..6e33ac976 100644 --- a/g10/import.c +++ b/g10/import.c @@ -3541,7 +3541,9 @@ list_standalone_revocation (ctrl_t ctrl, PKT_signature *sig, int sigrc) show_notation (sig, 3, 0, ((opt.list_options & LIST_SHOW_STD_NOTATIONS) ? 1 : 0) + - ((opt.list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : 0)); + ((opt.list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : 0) + + + ((opt.list_options & LIST_SHOW_HIDDEN_NOTATIONS) ? 4:0)); if (sig->flags.pref_ks && (opt.list_options & LIST_SHOW_KEYSERVER_URLS)) diff --git a/g10/keyedit.c b/g10/keyedit.c index b0f8ea5ed..1afaad6a9 100644 --- a/g10/keyedit.c +++ b/g10/keyedit.c @@ -299,7 +299,9 @@ keyedit_print_one_sig (ctrl_t ctrl, estream_t fp, ((opt. list_options & LIST_SHOW_STD_NOTATIONS) ? 1 : 0) + ((opt. - list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : 0)); + list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : 0) + + ((opt. + list_options & LIST_SHOW_HIDDEN_NOTATIONS) ? 4:0)); if (sig->flags.pref_ks && ((opt.list_options & LIST_SHOW_KEYSERVER_URLS) || extended)) diff --git a/g10/keylist.c b/g10/keylist.c index 7bd25de74..1c531126f 100644 --- a/g10/keylist.c +++ b/g10/keylist.c @@ -638,6 +638,7 @@ show_keyserver_url (PKT_signature * sig, int indent, int mode) * Defined bits in WHICH: * 1 - standard notations * 2 - user notations + * 4 - print notations normally hidden */ void show_notation (PKT_signature * sig, int indent, int mode, int which) @@ -653,6 +654,9 @@ show_notation (PKT_signature * sig, int indent, int mode, int which) /* There may be multiple notations in the same sig. */ for (nd = notations; nd; nd = nd->next) { + if (!(which & 4) && !strcmp (nd->name, "manu")) + continue; + if (mode != 2) { int has_at = !!strchr (nd->name, '@'); @@ -1522,11 +1526,11 @@ list_signature_print (ctrl_t ctrl, kbnode_t keyblock, kbnode_t node, if (sig->flags.notation && (opt.list_options & LIST_SHOW_NOTATIONS)) show_notation (sig, 3, 0, ((opt. - list_options & LIST_SHOW_STD_NOTATIONS) ? 1 : 0) - + - ((opt. - list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : - 0)); + list_options & LIST_SHOW_STD_NOTATIONS) ? 1 : 0) + + ((opt. + list_options & LIST_SHOW_USER_NOTATIONS) ? 2 : 0) + + ((opt. + list_options & LIST_SHOW_HIDDEN_NOTATIONS) ? 4 : 0)); if (sig->flags.notation && (opt.list_options diff --git a/g10/mainproc.c b/g10/mainproc.c index ebbe4a6a7..22d12799d 100644 --- a/g10/mainproc.c +++ b/g10/mainproc.c @@ -2492,7 +2492,9 @@ check_sig_and_print (CTX c, kbnode_t node) show_notation (sig, 0, 1, (((opt.verify_options&VERIFY_SHOW_STD_NOTATIONS)?1:0) - + ((opt.verify_options&VERIFY_SHOW_USER_NOTATIONS)?2:0))); + + ((opt.verify_options&VERIFY_SHOW_USER_NOTATIONS)?2:0) + + ((opt.verify_options &VERIFY_SHOW_HIDDEN_NOTATIONS)? 4:0) + )); else show_notation (sig, 0, 2, 0); } diff --git a/g10/options.h b/g10/options.h index fe81a0baf..cd5c19f45 100644 --- a/g10/options.h +++ b/g10/options.h @@ -400,6 +400,7 @@ EXTERN_UNLESS_MAIN_MODULE int memory_stat_debug_mode; #define COMPAT_PARALLELIZED 1 /* Use threaded hashing for signatures. */ #define COMPAT_T7014_OLD 2 /* Use initial T7014 test data. */ #define COMPAT_COMPR_KEYS 4 /* Allow import of compressed keys. (T7014) */ +#define COMPAT_NO_MANU 8 /* Do not include a "manu" notation. */ /* Compliance test macros. */ #define GNUPG (opt.compliance==CO_GNUPG || opt.compliance==CO_DE_VS) @@ -466,6 +467,7 @@ EXTERN_UNLESS_MAIN_MODULE int memory_stat_debug_mode; #define LIST_STORE_X509_NOTATIONS (1<<18) #define LIST_SHOW_OWNERTRUST (1<<19) #define LIST_SHOW_TRUSTSIG (1<<20) +#define LIST_SHOW_HIDDEN_NOTATIONS (1<<21) #define VERIFY_SHOW_PHOTOS (1<<0) #define VERIFY_SHOW_POLICY_URLS (1<<1) @@ -476,6 +478,7 @@ EXTERN_UNLESS_MAIN_MODULE int memory_stat_debug_mode; #define VERIFY_SHOW_UID_VALIDITY (1<<5) #define VERIFY_SHOW_UNUSABLE_UIDS (1<<6) #define VERIFY_SHOW_PRIMARY_UID_ONLY (1<<9) +#define VERIFY_SHOW_HIDDEN_NOTATIONS (1<<21) #define KEYSERVER_HTTP_PROXY (1<<0) #define KEYSERVER_TIMEOUT (1<<1) diff --git a/g10/packet.h b/g10/packet.h index e385966d3..8162ad802 100644 --- a/g10/packet.h +++ b/g10/packet.h @@ -914,6 +914,7 @@ void build_attribute_subpkt(PKT_user_id *uid,byte type, const void *buf,u32 buflen, const void *header,u32 headerlen); struct notation *string_to_notation(const char *string,int is_utf8); +struct notation *name_value_to_notation (const char *name, const char *value); struct notation *blob_to_notation(const char *name, const char *data, size_t len); struct notation *sig_to_notation(PKT_signature *sig); diff --git a/g10/sign.c b/g10/sign.c index 413a6025d..1e8bd8f95 100644 --- a/g10/sign.c +++ b/g10/sign.c @@ -66,18 +66,21 @@ typedef struct pt_extra_hash_data_s *pt_extra_hash_data_t; /* - * Create notations and other stuff. It is assumed that the strings in - * STRLIST are already checked to contain only printable data and have - * a valid NAME=VALUE format. + * Create notations and other stuff. It is assumed that the strings + * in STRLIST are already checked to contain only printable data and + * have a valid NAME=VALUE format. If with_manu is set a "manu" + * notation is also added: a value of 1 includes it in the standard + * way and a value of 23 assumes that the data is de-vs compliant. */ static void mk_notation_policy_etc (ctrl_t ctrl, PKT_signature *sig, - PKT_public_key *pk, PKT_public_key *pksk) + PKT_public_key *pk, PKT_public_key *pksk, int with_manu) { const char *string; char *p = NULL; strlist_t pu = NULL; struct notation *nd = NULL; + struct notation *ndmanu = NULL; struct expando_args args; log_assert (sig->version >= 4); @@ -94,6 +97,15 @@ mk_notation_policy_etc (ctrl_t ctrl, PKT_signature *sig, else if (IS_CERT(sig) && opt.cert_notations) nd = opt.cert_notations; + if (with_manu) + { + ndmanu = name_value_to_notation + ("manu", + gnupg_manu_notation_value (with_manu == 23? CO_DE_VS : CO_GNUPG)); + ndmanu->next = nd; + nd = ndmanu; + } + if (nd) { struct notation *item; @@ -113,6 +125,10 @@ mk_notation_policy_etc (ctrl_t ctrl, PKT_signature *sig, xfree (item->altvalue); item->altvalue = NULL; } + /* Restore the original nd and release ndmanu. */ + nd = ndmanu; + ndmanu->next = NULL; + free_notation (ndmanu); } /* Set policy URL. */ @@ -920,7 +936,7 @@ write_plaintext_packet (iobuf_t out, iobuf_t inp, /* * Write the signatures from the SK_LIST to OUT. HASH must be a * non-finalized hash which will not be changes here. EXTRAHASH is - * either NULL or the extra data tro be hashed into v5 signatures. + * either NULL or the extra data to be hashed into v5 signatures. */ static int write_signature_packets (ctrl_t ctrl, @@ -930,6 +946,7 @@ write_signature_packets (ctrl_t ctrl, int status_letter, const char *cache_nonce) { SK_LIST sk_rover; + int with_manu; /* Loop over the certificates with secret keys. */ for (sk_rover = sk_list; sk_rover; sk_rover = sk_rover->next) @@ -966,7 +983,16 @@ write_signature_packets (ctrl_t ctrl, BUG (); build_sig_subpkt_from_sig (sig, pk, 0); - mk_notation_policy_etc (ctrl, sig, NULL, pk); + + if (opt.compliance == CO_DE_VS + && gnupg_rng_is_compliant (CO_DE_VS)) + with_manu = 23; /* FIXME: Also check that the algos are compliant?*/ + else if (!(opt.compat_flags & COMPAT_NO_MANU)) + with_manu = 1; + else + with_manu = 0; + + mk_notation_policy_etc (ctrl, sig, NULL, pk, with_manu); if (opt.flags.include_key_block && IS_SIG (sig)) err = mk_sig_subpkt_key_block (ctrl, sig, pk); else @@ -1813,6 +1839,7 @@ make_keysig_packet (ctrl_t ctrl, gcry_md_hd_t md; u32 pk_keyid[2], pksk_keyid[2]; unsigned int signhints; + int with_manu; log_assert ((sigclass&~3) == SIGCLASS_CERT || sigclass == SIGCLASS_KEY @@ -1884,7 +1911,20 @@ make_keysig_packet (ctrl_t ctrl, sig->sig_class = sigclass; build_sig_subpkt_from_sig (sig, pksk, signhints); - mk_notation_policy_etc (ctrl, sig, pk, pksk); + + with_manu = 0; + if ((signhints & SIGNHINT_SELFSIG) /* Only for self-signatures. */ + && ((sigclass&~3) == SIGCLASS_CERT /* on UIDs and subkeys. */ + || sigclass == SIGCLASS_SUBKEY)) + { + if (opt.compliance == CO_DE_VS + && gnupg_rng_is_compliant (CO_DE_VS)) + with_manu = 23; /* Always in de-vs mode. */ + else if (!(opt.compat_flags & COMPAT_NO_MANU)) + with_manu = 1; + } + + mk_notation_policy_etc (ctrl, sig, pk, pksk, with_manu); /* Crucial that the call to mksubpkt comes LAST before the calls * to finalize the sig as that makes it possible for the mksubpkt diff --git a/sm/keylist.c b/sm/keylist.c index 41e7ca309..faa515ef5 100644 --- a/sm/keylist.c +++ b/sm/keylist.c @@ -179,6 +179,7 @@ static struct /* GnuPG extensions */ { "1.3.6.1.4.1.11591.2.1.1", "pkaAddress" }, + { "1.3.6.1.4.1.11591.2.1.2", "manuNotation" }, { "1.3.6.1.4.1.11591.2.2.1", "standaloneCertificate" }, { "1.3.6.1.4.1.11591.2.2.2", "wellKnownPrivateKey" }, { "1.3.6.1.4.1.11591.2.6.1", "gpgUsageCert", OID_FLAG_KP }, -- cgit v1.2.3