Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F18826252
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
20 KB
Subscribers
None
View Options
diff --git a/tests/openpgp/Makefile.am b/tests/openpgp/Makefile.am
index 921619f8f..5c4c3703d 100644
--- a/tests/openpgp/Makefile.am
+++ b/tests/openpgp/Makefile.am
@@ -1,163 +1,157 @@
# Makefile.am - For tests/openpgp
# Copyright (C) 1998, 1999, 2000, 2001, 2003,
# 2010 Free Software Foundation, Inc.
#
# This file is part of GnuPG.
#
# GnuPG is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# GnuPG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
# Process this file with automake to create Makefile.in
# Programs required before we can run these tests.
required_pgms = ../../g10/gpg$(EXEEXT) ../../agent/gpg-agent$(EXEEXT) \
../../tools/gpg-connect-agent$(EXEEXT) \
../../tools/mk-tdata$(EXEEXT) \
../gpgscm/gpgscm$(EXEEXT)
AM_CPPFLAGS = -I$(top_srcdir)/common
include $(top_srcdir)/am/cmacros.am
AM_CFLAGS =
noinst_PROGRAMS = fake-pinentry
fake_pinentry_SOURCES = fake-pinentry.c
TESTS_ENVIRONMENT = GNUPGHOME=$(abs_builddir) GPG_AGENT_INFO= LC_ALL=C \
EXEEXT=$(EXEEXT) \
PATH=../gpgscm:$(PATH) \
objdir=$(abs_top_builddir) \
GPGSCM_PATH=$(top_srcdir)/tests/gpgscm:$(top_srcdir)/tests/openpgp
-if SQLITE3
-sqlite3_dependent_tests = tofu.test
-else
-sqlite3_dependent_tests =
-endif
-
# Note: setup.scm needs to be the first test to run and finish.scm
# the last one
TESTS = setup.scm \
version.scm \
mds.scm \
decrypt.scm \
decrypt-dsa.scm \
sigs.scm \
sigs-dsa.scm \
encrypt.scm \
encrypt-dsa.scm \
seat.scm \
clearsig.scm \
encryptp.scm \
detach.scm \
detachm.scm \
armsigs.scm \
armencrypt.scm \
armencryptp.scm \
signencrypt.scm \
signencrypt-dsa.scm \
armsignencrypt.scm \
armdetach.scm \
armdetachm.scm \
genkey1024.scm \
conventional.scm \
conventional-mdc.scm \
multisig.scm \
verify.scm \
armor.scm \
import.scm \
ecc.scm \
4gb-packet.scm \
- $(sqlite3_dependent_tests) \
+ tofu.scm \
gpgtar.scm \
use-exact-key.scm \
default-key.scm \
- export.test \
+ export.scm \
finish.scm
TEST_FILES = pubring.asc secring.asc plain-1o.asc plain-2o.asc plain-3o.asc \
plain-1.asc plain-2.asc plain-3.asc plain-1-pgp.asc \
plain-largeo.asc \
pubring.pkr.asc secring.skr.asc secdemo.asc pubdemo.asc \
gpg.conf.tmpl gpg-agent.conf.tmpl \
bug537-test.data.asc bug894-test.asc \
bug1223-good.asc bug1223-bogus.asc 4gb-packet.asc \
tofu-keys.asc tofu-keys-secret.asc \
tofu-2183839A-1.txt tofu-BC15C85A-1.txt tofu-EE37CF96-1.txt
data_files = data-500 data-9000 data-32000 data-80000 plain-large
priv_keys = privkeys/50B2D4FA4122C212611048BC5FC31BD44393626E.asc \
privkeys/7E201E28B6FEB2927B321F443205F4724EBE637E.asc \
privkeys/13FDB8809B17C5547779F9D205C45F47CE0217CE.asc \
privkeys/343D8AF79796EE107D645A2787A9D9252F924E6F.asc \
privkeys/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.asc \
privkeys/0D6F6AD4C4C803B25470F9104E9F4E6A4CA64255.asc \
privkeys/FD692BD59D6640A84C8422573D469F84F3B98E53.asc \
privkeys/76F7E2B35832976B50A27A282D9B87E44577EB66.asc \
privkeys/A0747D5F9425E6664F4FFBEED20FBCA79FDED2BD.asc \
privkeys/0DD40284FF992CD24DC4AAC367037E066FCEE26A.asc \
privkeys/2BC997C0B8691D41D29A4EC81CCBCF08454E4961.asc \
privkeys/3C9D5ECA70130C2DBB1FC6AC0076BEEEC197716F.asc \
privkeys/449E644892C951A37525654730DD32C202079926.asc \
privkeys/58FFE844087634E62440224908BDE44BEA7EB730.asc \
privkeys/4DF9172D6FF428C97A0E9AA96F03E8BCE3B2F188.asc \
privkeys/9D7CD8F53F2F14C3E2177D1E9D1D11F39513A4A4.asc \
privkeys/6E6B7ED0BD4425018FFC54F3921D5467A3AE00EB.asc \
privkeys/C905D0AB6AE9655C5A35975939997BBF3325D6DD.asc \
privkeys/B2BAA7144303DF19BB6FDE23781DD3FDD97918D4.asc \
privkeys/CF60965BF51F67CF80DECE853E0D2D343468571D.asc \
privkeys/DF00E361D34F80868D06879AC21D7A7D4E4FAD76.asc \
privkeys/00FE67F28A52A8AA08FFAED20AF832DA916D1985.asc \
privkeys/1DF48228FEFF3EC2481B106E0ACA8C465C662CC5.asc \
privkeys/A2832820DC9F40751BDCD375BB0945BA33EC6B4C.asc \
privkeys/ADE710D74409777B7729A7653373D820F67892E0.asc \
privkeys/CEFC51AF91F68A2904FBFF62C4F075A4785B803F.asc
sample_keys = samplekeys/ecc-sample-1-pub.asc \
samplekeys/ecc-sample-2-pub.asc \
samplekeys/ecc-sample-3-pub.asc \
samplekeys/ecc-sample-1-sec.asc \
samplekeys/ecc-sample-2-sec.asc \
samplekeys/ecc-sample-3-sec.asc \
samplekeys/eddsa-sample-1-pub.asc \
samplekeys/eddsa-sample-1-sec.asc \
samplekeys/dda252ebb8ebe1af-1.asc \
samplekeys/dda252ebb8ebe1af-2.asc \
samplekeys/whats-new-in-2.1.asc \
samplekeys/e2e-p256-1-clr.asc \
samplekeys/e2e-p256-1-prt.asc \
samplekeys/E657FB607BB4F21C90BB6651BC067AF28BC90111.asc
EXTRA_DIST = defs.inc defs.scm pinentry.sh $(TESTS) $(TEST_FILES) \
mkdemodirs signdemokey $(priv_keys) $(sample_keys) \
ChangeLog-2011
CLEANFILES = prepared.stamp x y yy z out err $(data_files) \
plain-1 plain-2 plain-3 trustdb.gpg *.lock .\#lk* \
*.test.log gpg_dearmor gpg.conf gpg-agent.conf S.gpg-agent \
pubring.gpg pubring.gpg~ pubring.kbx pubring.kbx~ \
secring.gpg pubring.pkr secring.skr \
gnupg-test.stop random_seed gpg-agent.log tofu.db \
passphrases
clean-local:
-rm -rf private-keys-v1.d openpgp-revocs.d tofu.d gpgtar.d
# We need to depend on a couple of programs so that the tests don't
# start before all programs are built.
all-local: $(required_pgms)
diff --git a/tests/openpgp/defs.scm b/tests/openpgp/defs.scm
index 6fdb95580..4257b286e 100644
--- a/tests/openpgp/defs.scm
+++ b/tests/openpgp/defs.scm
@@ -1,130 +1,131 @@
;; Common definitions for the OpenPGP test scripts.
;;
;; Copyright (C) 2016 g10 Code GmbH
;;
;; This file is part of GnuPG.
;;
;; GnuPG is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3 of the License, or
;; (at your option) any later version.
;;
;; GnuPG is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, see <http://www.gnu.org/licenses/>.
;;
;; Constants.
;;
(define usrname1 "one@example.com")
(define usrpass1 "def")
(define usrname2 "two@example.com")
(define usrpass2 "")
(define usrname3 "three@example.com")
(define usrpass3 "")
(define dsa-usrname1 "pgp5")
;; we use the sub key because we do not yet have the logic to to derive
;; the first encryption key from a keyblock (I guess) (Well of course
;; we have this by now and the notation below will lookup the primary
;; first and then search for the encryption subkey.)
(define dsa-usrname2 "0xCB879DE9")
(define plain-files '("plain-1" "plain-2" "plain-3"))
(define data-files '("data-500" "data-9000" "data-32000" "data-80000"))
(define exp-files '())
(define (qualify executable)
(string-append executable (getenv "EXEEXT")))
(define (getenv' key default)
(let ((value (getenv key)))
(if (string=? "" value)
default
value)))
(define tools
'((gpg "GPG" "g10/gpg")
(gpg-agent "GPG_AGENT" "agent/gpg-agent")
(gpg-connect-agent "GPG_CONNECT_AGENT" "tools/gpg-connect-agent")
(gpgconf "GPGCONF" "tools/gpgconf")
(gpg-preset-passphrase "GPG_PRESET_PASSPHRASE"
"agent/gpg-preset-passphrase")
(mktdata "MKTDATA" "tools/mk-tdata")
(gpgtar "GPGTAR" "tools/gpgtar")
(gpg-zip "GPGZIP" "tools/gpg-zip")))
(define (tool which)
(let ((t (assoc which tools))
(prefix (getenv "BIN_PREFIX")))
(getenv' (cadr t)
(qualify (if (string=? prefix "")
(string-append (getenv "objdir") "/" (caddr t))
(string-append prefix "/" (basename (caddr t))))))))
(define have-opt-always-trust
(string-contains? (call-popen `(,(tool 'gpg) --dump-options) "")
"--always-trust"))
(define GPG `(,(tool 'gpg) --no-permission-warning
,@(if have-opt-always-trust '(--always-trust) '())))
(define PINENTRY (string-append (getcwd) "/" (qualify "fake-pinentry")))
(define (tr:gpg input args)
(tr:spawn input `(,@GPG --output **out** ,@args **in**)))
(define (pipe:gpg args)
(pipe:spawn `(,@GPG --output - ,@args -)))
+(define (gpg-with-colons args)
+ (let ((s (call-popen `(,@GPG --with-colons ,@args) "")))
+ (map (lambda (line) (string-split line #\:))
+ (string-split s #\newline))))
+
(define (get-config what)
- (let* ((config-string
- (call-popen `(,@GPG --with-colons --list-config ,what) ""))
- (config (string-splitn
- (string-rtrim char-whitespace? config-string) #\: 2)))
- (string-split (caddr config) #\;)))
+ (string-split (caddar (gpg-with-colons `(--list-config ,what))) #\;))
(define all-pubkey-algos (get-config "pubkeyname"))
(define all-hash-algos (get-config "digestname"))
(define all-cipher-algos (get-config "ciphername"))
(define (have-pubkey-algo? x)
(not (not (member x all-pubkey-algos))))
(define (have-hash-algo? x)
(not (not (member x all-hash-algos))))
(define (have-cipher-algo? x)
(not (not (member x all-cipher-algos))))
(define (gpg-pipe args0 args1 errfd)
(lambda (source sink)
(let* ((p (pipe))
(task0 (spawn-process-fd `(,@GPG ,@args0)
source (:write-end p) errfd))
(_ (close (:write-end p)))
(task1 (spawn-process-fd `(,@GPG ,@args1)
(:read-end p) sink errfd)))
(close (:read-end p))
(wait-processes (list GPG GPG) (list task0 task1) #t))))
(setenv "GPG_AGENT_INFO" "" #t)
(setenv "GNUPGHOME" (getcwd) #t)
;;
;; GnuPG helper.
;;
;; Call GPG to obtain the hash sums. Either specify an input file in
;; ARGS, or an string in INPUT. Returns a list of (<algo>
;; "<hashsum>") lists.
(define (gpg-hash-string args input)
(map
(lambda (line)
(let ((p (string-split line #\:)))
(list (string->number (cadr p)) (caddr p))))
(string-split
(call-popen `(,@GPG --with-colons ,@args) input) #\newline)))
diff --git a/tests/openpgp/export.scm b/tests/openpgp/export.scm
new file mode 100755
index 000000000..829170541
--- /dev/null
+++ b/tests/openpgp/export.scm
@@ -0,0 +1,99 @@
+#!/usr/bin/env gpgscm
+
+;; Copyright (C) 2016 g10 Code GmbH
+;;
+;; This file is part of GnuPG.
+;;
+;; GnuPG is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; GnuPG is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+(load (with-path "defs.scm"))
+
+(define (check-for predicate lines message)
+ (unless (any predicate lines)
+ (error message)))
+
+(define (check-exported-key dump keyid)
+ (check-for (lambda (l)
+ (and (string-prefix? l " keyid: ")
+ (string-suffix? l keyid))) dump
+ "Keyid not found")
+ (check-for (lambda (l) (string-prefix? l ":user ID packet:")) dump
+ "User ID packet not found")
+ (check-for (lambda (l)
+ (and (string-prefix? l ":signature packet:")
+ (string-contains? l "keyid")
+ (string-suffix? l keyid))) dump
+ "Signature packet not found"))
+
+(define (check-exported-public-key packet-dump keyid)
+ (let ((dump (string-split packet-dump #\newline)))
+ (check-for (lambda (l) (string-prefix? l ":public key packet:")) dump
+ "Public key packet not found")
+ (check-exported-key dump keyid)))
+
+(define (check-exported-private-key packet-dump keyid)
+ (let ((dump (string-split packet-dump #\newline)))
+ (check-for (lambda (l) (string-prefix? l ":secret key packet:")) dump
+ "Secret key packet not found")
+ (check-exported-key dump keyid)))
+
+(lettmp
+ ;; Prepare two temporary files for communication with the fake
+ ;; pinentry program.
+ (logfile ppfile)
+
+ (define (prepare-passphrases . passphrases)
+ (call-with-output-file ppfile
+ (lambda (port)
+ (for-each (lambda (passphrase)
+ (display passphrase port)
+ (display #\newline port)) passphrases))))
+
+ (define CONFIRM "fake-entry being started to CONFIRM the weak phrase")
+
+ (define (assert-passphrases-consumed)
+ (call-with-input-file ppfile
+ (lambda (port)
+ (unless
+ (eof-object? (peek-char port))
+ (error (string-append
+ "Expected all passphrases to be consumed, but found: "
+ (read-all port)))))))
+
+ (setenv "PINENTRY_USER_DATA"
+ (string-append "--logfile=" logfile " --passphrasefile=" ppfile) #t)
+
+ (for-each-p
+ "Checking key export"
+ (lambda (keyid)
+ (tr:do
+ (tr:pipe-do
+ (pipe:gpg `(--export ,keyid))
+ (pipe:gpg '(--list-packets)))
+ (tr:call-with-content check-exported-public-key keyid))
+
+ (if (string=? "D74C5F22" keyid)
+ ;; Key D74C5F22 is protected by a passphrase. Prepare this
+ ;; one. Currently, GnuPG does not ask for an export passphrase
+ ;; in this case.
+ (prepare-passphrases usrpass1))
+
+ (tr:do
+ (tr:pipe-do
+ (pipe:gpg `(--export-secret-keys ,keyid))
+ (pipe:gpg '(--list-packets)))
+ (tr:call-with-content check-exported-private-key keyid))
+
+ (assert-passphrases-consumed))
+ '("D74C5F22" "C40FDECF" "ECABF51D")))
diff --git a/tests/openpgp/tofu.scm b/tests/openpgp/tofu.scm
new file mode 100755
index 000000000..24fa9df02
--- /dev/null
+++ b/tests/openpgp/tofu.scm
@@ -0,0 +1,165 @@
+#!/usr/bin/env gpgscm
+
+;; Copyright (C) 2016 g10 Code GmbH
+;;
+;; This file is part of GnuPG.
+;;
+;; GnuPG is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; GnuPG is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+(load (with-path "defs.scm"))
+
+(define GPG `(,(tool 'gpg) --no-permission-warning)) ;; w/o --always-trust
+(define GNUPGHOME (getenv "GNUPGHOME"))
+(if (string=? "" GNUPGHOME)
+ (error "GNUPGHOME not set"))
+
+(catch (skip "Tofu not supported")
+ (call-check `(,@GPG --trust-model=tofu --list-config)))
+
+(define KEYS '("2183839A" "BC15C85A" "EE37CF96"))
+
+;; Import the test keys.
+(call-check `(,@GPG --import ,(in-srcdir "tofu-keys.asc")))
+
+;; Make sure the keys are imported.
+(for-each (lambda (keyid)
+ (catch (error "Missing key" keyid)
+ (call-check `(,@GPG --list-keys ,keyid))))
+ KEYS)
+
+;; Get tofu policy for KEYID. Any remaining arguments are simply
+;; passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (getpolicy keyid format . args)
+ (let ((policy
+ (list-ref (assoc "uid" (gpg-with-colons
+ `(--tofu-db-format ,format
+ --trust-model=tofu
+ ,@args
+ --list-keys ,keyid))) 17)))
+ (unless (member policy '("auto" "good" "unknown" "bad" "ask"))
+ (error "Bad policy:" policy))
+ policy))
+
+;; Check that KEYID's tofu policy matches EXPECTED-POLICY. Any
+;; remaining arguments are simply passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (checkpolicy keyid format expected-policy . args)
+ (let ((policy (apply getpolicy `(,keyid ,format ,@args))))
+ (unless (string=? policy expected-policy)
+ (error keyid ": Expected policy to be" expected-policy
+ "but got" policy))))
+
+;; Get the trust level for KEYID. Any remaining arguments are simply
+;; passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (gettrust keyid format . args)
+ (let ((trust
+ (list-ref (assoc "pub" (gpg-with-colons
+ `(--tofu-db-format ,format
+ --trust-model=tofu
+ ,@args
+ --list-keys ,keyid))) 1)))
+ (unless (and (= 1 (string-length trust))
+ (member (string-ref trust 0) (string->list "oidreqnmfuws-")))
+ (error "Bad trust value:" trust))
+ trust))
+
+;; Check that KEYID's trust level matches EXPECTED-TRUST. Any
+;; remaining arguments are simply passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (checktrust keyid format expected-trust . args)
+ (let ((trust (apply gettrust `(,keyid ,format ,@args))))
+ (unless (string=? trust expected-trust)
+ (error keyid ": Expected trust to be" expected-trust
+ "but got" trust))))
+
+;; Set key KEYID's policy to POLICY. Any remaining arguments are
+;; passed as options to gpg.
+(define (setpolicy keyid format policy . args)
+ (call-check `(,@GPG --tofu-db-format ,format
+ --trust-model=tofu ,@args
+ --tofu-policy ,policy ,keyid)))
+
+(for-each-p
+ "Testing tofu db formats"
+ (lambda (format)
+ ;; Carefully remove the TOFU db.
+ (catch '() (unlink (string-append GNUPGHOME "/tofu.db")))
+ (catch '() (unlink-recursively (string-append GNUPGHOME "/tofu.d")))
+
+ ;; Verify a message. There should be no conflict and the trust
+ ;; policy should be set to auto.
+ (call-check `(,@GPG --tofu-db-format ,format --trust-model=tofu
+ --verify ,(in-srcdir "tofu-2183839A-1.txt")))
+
+ (checkpolicy "2183839A" format "auto")
+ ;; Check default trust.
+ (checktrust "2183839A" format "m")
+
+ ;; Trust should be derived lazily. Thus, if the policy is set to
+ ;; auto and we change --tofu-default-policy, then the trust should
+ ;; change as well. Try it.
+ (checktrust "2183839A" format "f" '--tofu-default-policy=good)
+ (checktrust "2183839A" format "-" '--tofu-default-policy=unknown)
+ (checktrust "2183839A" format "n" '--tofu-default-policy=bad)
+
+ ;; Change the policy to something other than auto and make sure the
+ ;; policy and the trust are correct.
+ (for-each-p
+ ""
+ (lambda (policy)
+ (let ((expected-trust
+ (cond
+ ((string=? "good" policy) "f")
+ ((string=? "unknown" policy) "-")
+ (else "n"))))
+ (setpolicy "2183839A" format policy)
+
+ ;; Since we have a fixed policy, the trust level shouldn't
+ ;; change if we change the default policy.
+ (for-each-p
+ ""
+ (lambda (default-policy)
+ (checkpolicy "2183839A" format policy
+ '--tofu-default-policy default-policy)
+ (checktrust "2183839A" format expected-trust
+ '--tofu-default-policy default-policy))
+ '("auto" "good" "unknown" "bad" "ask"))))
+ '("good" "unknown" "bad"))
+
+ ;; BC15C85A conflicts with 2183839A. On conflict, this will set
+ ;; BC15C85A to ask. If 2183839A is auto (it's not, it's bad), then
+ ;; it will be set to ask.
+ (call-check `(,@GPG --tofu-db-format ,format --trust-model=tofu
+ --verify ,(in-srcdir "tofu-BC15C85A-1.txt")))
+ (checkpolicy "BC15C85A" format "ask")
+ (checkpolicy "2183839A" format "bad")
+
+ ;; EE37CF96 conflicts with 2183839A and BC15C85A. We change
+ ;; BC15C85A's policy to auto and leave 2183839A's policy at bad.
+ ;; This conflict should cause BC15C85A's policy to be changed to
+ ;; ask (since it is auto), but not affect 2183839A's policy.
+ (setpolicy "BC15C85A" format "auto")
+ (checkpolicy "BC15C85A" format "auto")
+ (call-check `(,@GPG --tofu-db-format ,format --trust-model=tofu
+ --verify ,(in-srcdir "tofu-EE37CF96-1.txt")))
+ (checkpolicy "BC15C85A" format "ask")
+ (checkpolicy "2183839A" format "bad")
+ (checkpolicy "EE37CF96" format "ask"))
+ '("split" "flat"))
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 4:51 PM (14 h, 52 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
21/54/291002c48ab96a6448f595277408
Attached To
rG GnuPG
Event Timeline
Log In to Comment