Skip to content

Commit

Permalink
Implemented trust store and added official DCL trusted PAAs (project-…
Browse files Browse the repository at this point in the history
  • Loading branch information
vijs authored and andrei-menzopol committed Apr 14, 2022
1 parent 28e8490 commit b19fcd7
Show file tree
Hide file tree
Showing 35 changed files with 504 additions and 35 deletions.
131 changes: 131 additions & 0 deletions credentials/development/fetch-development-paa-certs-from-dcl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/python

#
# Copyright (c) 2022 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# Script that was used to fetch CHIP Development Product Attestation Authority (PAA)
# certificates from DCL.
# The script expects the path to the dcld tool binary as an input argument.
#
# Usage example when the script is run from the CHIP SDK root directory:
# python ./credentials/development/fetch-development-paa-certs-from-dcl.py /path/to/dcld
#
# The result will be stored in:
# credentials/development/paa-root-certs
#

import os
import sys
import subprocess
import copy
import re
from cryptography.hazmat.primitives import serialization
from cryptography import x509


def parse_paa_root_certs(cmdpipe, paa_list):
"""
example output of a query to all x509 root certs in DCL:
certs:
- subject: CN=Non Production ONLY - XFN PAA Class 3
subject_key_id: F8:99:A9:D5:AD:71:71:E4:C3:81:7F:14:10:7F:78:F0:D9:F7:62:E9
- subject: CN=Matter Development PAA
subject_key_id: FA:92:CF:9:5E:FA:42:E1:14:30:65:16:32:FE:FE:1B:2C:77:A7:C8
- subject: CN=Matter PAA 1,O=Google,C=US,1.3.6.1.4.1.37244.2.1=#130436303036
subject_key_id: B0:0:56:81:B8:88:62:89:62:80:E1:21:18:A1:A8:BE:9:DE:93:21
- subject: CN=Matter Test PAA,1.3.6.1.4.1.37244.2.1=#130431323544
subject_key_id: E2:90:8D:36:9C:3C:A3:C1:13:BB:9:E2:4D:C1:CC:C5:A6:66:91:D4
Brief:
This method will search for the first line that contains ': ' char sequence.
From there, it assumes every 2 lines contain subject and subject key id info of
a valid PAA root certificate.
The paa_list parameter will contain a list of all valid PAA Root certificates
from DCL.
"""

result = {}

while True:
line = cmdpipe.stdout.readline()
if not line:
break
else:
if b': ' in line:
key, value = line.split(b': ')
result[key.strip(b' -')] = value.strip()
parse_paa_root_certs.counter += 1
if parse_paa_root_certs.counter % 2 == 0:
paa_list.append(copy.deepcopy(result))


def write_paa_root_cert(cmdpipe, subject):
filename = 'paa-root-certs/dcld_mirror_' + \
re.sub('[^a-zA-Z0-9_-]', '', re.sub('[=, ]', '_', subject))
with open(filename + '.pem', 'wb+') as outfile:
while True:
line = cmdpipe.stdout.readline()
if not line:
break
else:
if b'pem_cert: |' in line:
while True:
line = cmdpipe.stdout.readline()
outfile.write(line.strip(b' \t'))
if b'-----END CERTIFICATE-----' in line:
break
# convert pem file to der
with open(filename + '.pem', 'rb') as infile:
pem_certificate = x509.load_pem_x509_certificate(infile.read())
with open(filename + '.der', 'wb+') as outfile:
der_certificate = pem_certificate.public_bytes(
serialization.Encoding.DER)
outfile.write(der_certificate)


def main():
if len(sys.argv) == 2:
dcld = sys.argv[1]
else:
sys.exit(
"Error: Please specify exactly one input argument; the path to the dcld tool binary")

previous_dir = os.getcwd()
abspath = os.path.dirname(sys.argv[0])
os.chdir(abspath)

os.makedirs('paa-root-certs', exist_ok=True)

cmdpipe = subprocess.Popen([dcld, 'query', 'pki', 'all-x509-root-certs'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)

paa_list = []
parse_paa_root_certs.counter = 0
parse_paa_root_certs(cmdpipe, paa_list)

for paa in paa_list:
cmdpipe = subprocess.Popen(
[dcld, 'query', 'pki', 'x509-cert', '-u',
paa[b'subject'].decode("utf-8"), '-k', paa[b'subject_key_id'].decode("utf-8")],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
write_paa_root_cert(cmdpipe, paa[b'subject'].decode("utf-8"))

os.chdir(previous_dir)


if __name__ == "__main__":
main()
Binary file not shown.
12 changes: 12 additions & 0 deletions credentials/development/paa-root-certs/Chip-Test-PAA-FFF1-Cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBvTCCAWSgAwIBAgIITqjoMYLUHBwwCgYIKoZIzj0EAwIwMDEYMBYGA1UEAwwP
TWF0dGVyIFRlc3QgUEFBMRQwEgYKKwYBBAGConwCAQwERkZGMTAgFw0yMTA2Mjgx
NDIzNDNaGA85OTk5MTIzMTIzNTk1OVowMDEYMBYGA1UEAwwPTWF0dGVyIFRlc3Qg
UEFBMRQwEgYKKwYBBAGConwCAQwERkZGMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABLbLY3KIfyko9brIGqnZOuJDHK2p154kL2UXfvnO2TKijs0Duq9qj8oYShpQ
NUKWDUU/MD8fGUIddR6Pjxqam3WjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD
VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRq/SJ3H1Ef7L8WQZdnENzcMaFxfjAfBgNV
HSMEGDAWgBRq/SJ3H1Ef7L8WQZdnENzcMaFxfjAKBggqhkjOPQQDAgNHADBEAiBQ
qoAC9NkyqaAFOPZTaK0P/8jvu8m+t9pWmDXPmqdRDgIgI7rI/g8j51RFtlM5CBpH
mUkpxyqvChVI1A0DTVFLJd4=
-----END CERTIFICATE-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBkTCCATegAwIBAgIHC4+6qN2G7jAKBggqhkjOPQQDAjAaMRgwFgYDVQQDDA9N
YXR0ZXIgVGVzdCBQQUEwIBcNMjEwNjI4MTQyMzQzWhgPOTk5OTEyMzEyMzU5NTla
MBoxGDAWBgNVBAMMD01hdHRlciBUZXN0IFBBQTBZMBMGByqGSM49AgEGCCqGSM49
AwEHA0IABBDvAqgah7aBIfuo0xl4+AejF+UKqKgoRGgokUuTPejt1KXDnJ/3Gkzj
ZH/X9iZTt9JJX8ukwPR/h2iAA54HIEqjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQEw
DgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR4XOcFuGuPTm/Hk6pgy0PqaWiC1TAf
BgNVHSMEGDAWgBR4XOcFuGuPTm/Hk6pgy0PqaWiC1TAKBggqhkjOPQQDAgNIADBF
AiEAue/bPqBqUuwL8B5h2u0sLRVt22zwFBAdq3mPrAX6R+UCIGAGHT411g2dSw1E
ja12EvfoXFguP8MS3Bh5TdNzcV5d
-----END CERTIFICATE-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBoDCCAUagAwIBAgIIV9Oi0B4xgZAwCgYIKoZIzj0EAwIwITEfMB0GA1UEAwwW
TWF0dGVyIERldmVsb3BtZW50IFBBQTAgFw0yMTA2MjgxNDIzNDNaGA85OTk5MTIz
MTIzNTk1OVowITEfMB0GA1UEAwwWTWF0dGVyIERldmVsb3BtZW50IFBBQTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABBsPJZQuPZKr1nBMGieBoDjsUyEsTatYsL48
QL37SSMjQhx53MetcBgQBxINyG8KiSU9iZPrN6tlLvjbE3XlsUWjZjBkMBIGA1Ud
EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBT6ks8JXvpC
4RQwZRYy/v4bLHenyDAfBgNVHSMEGDAWgBT6ks8JXvpC4RQwZRYy/v4bLHenyDAK
BggqhkjOPQQDAgNIADBFAiBQp5AzZLZT/w6kY9xoSobdJccxo57+s8IM0t7RtmB+
LwIhAK/U7UtqmeX4xVIdcB68+f1TuTlP2A/FmZL/Plu7tgo1
-----END CERTIFICATE-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB7TCCAZOgAwIBAgIBATAKBggqhkjOPQQDAjBLMQswCQYDVQQGEwJVUzEPMA0G
A1UECgwGR29vZ2xlMRUwEwYDVQQDDAxNYXR0ZXIgUEFBIDExFDASBgorBgEEAYKi
fAIBDAQ2MDA2MCAXDTIxMTIwODIwMjYwM1oYDzIxMjExMjA4MjAyNjAzWjBLMQsw
CQYDVQQGEwJVUzEPMA0GA1UECgwGR29vZ2xlMRUwEwYDVQQDDAxNYXR0ZXIgUEFB
IDExFDASBgorBgEEAYKifAIBDAQ2MDA2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
QgAE8iZX+exx8NDV7jYKorx3EcsD1gessexUTSimIfvFI2PySlReMjJDVCGIzXor
hTYFOzwMAx4b6ogNMIUmcW7uT6NmMGQwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLAAVoG4iGKJYoDhIRihqL4J3pMhMB8GA1Ud
IwQYMBaAFLAAVoG4iGKJYoDhIRihqL4J3pMhMAoGCCqGSM49BAMCA0gAMEUCIQCV
c26cVlyqjhQfcgN3udpne6zZQdyVMNLRWZn3EENBkAIgasUeFU8zaUt8bKNWd0k+
4RQp5Cp5wYzrE8AxJ9BiA/E=
-----END CERTIFICATE-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIByjCCAXCgAwIBAgIUFkdW6XaPDQDsJ3530eRkiOtYbWQwCgYIKoZIzj0EAwIw
MDEuMCwGA1UEAwwlTm9uIFByb2R1Y3Rpb24gT05MWSAtIFhGTiBQQUEgQ2xhc3Mg
MzAgFw0yMTEyMTQwMzI3MzZaGA8yMDUxMTIwNzAzMjczNlowMDEuMCwGA1UEAwwl
Tm9uIFByb2R1Y3Rpb24gT05MWSAtIFhGTiBQQUEgQ2xhc3MgMzBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABB+Unq8KdMuQ6xWFKtAVGreDGzDlyLrpuSIZ86eMswgu
4xvjijYN6iljia1HjxVTTRdieROa7mpoLD7qEUC5yjmjZjBkMBIGA1UdEwEB/wQI
MAYBAf8CAQEwHwYDVR0jBBgwFoAU+Jmp1a1xceTDgX8UEH948Nn3YukwHQYDVR0O
BBYEFPiZqdWtcXHkw4F/FBB/ePDZ92LpMA4GA1UdDwEB/wQEAwIBhjAKBggqhkjO
PQQDAgNIADBFAiBYIsjeauI2nDknU1ThEDzyGfg4F9tLSkiuTrTJGr5EqQIhAMFX
bxTzgOfx0RPgpEU8syFEYyXCBcv4hV14rWddc08G
-----END CERTIFICATE-----
1 change: 1 addition & 0 deletions examples/chip-tool/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ static_library("chip-tool-utils") {
"${chip_root}/src/app/tests/suites/commands/system",
"${chip_root}/src/app/tests/suites/pics",
"${chip_root}/src/controller/data_model",
"${chip_root}/src/credentials:file_attestation_trust_store",
"${chip_root}/src/lib",
"${chip_root}/src/platform",
"${chip_root}/third_party/inipp",
Expand Down
30 changes: 28 additions & 2 deletions examples/chip-tool/commands/common/CHIPCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <controller/CHIPDeviceControllerFactory.h>
#include <core/CHIPBuildConfig.h>
#include <credentials/attestation_verifier/FileAttestationTrustStore.h>
#include <lib/core/CHIPVendorIdentifiers.hpp>
#include <lib/support/CodeUtils.h>
#include <lib/support/ScopedBuffer.h>
Expand All @@ -36,6 +37,22 @@ constexpr chip::FabricId kIdentityAlphaFabricId = 1;
constexpr chip::FabricId kIdentityBetaFabricId = 2;
constexpr chip::FabricId kIdentityGammaFabricId = 3;

namespace {
const chip::Credentials::AttestationTrustStore * GetTestFileAttestationTrustStore(const char * paaTrustStorePath)
{
static chip::Credentials::FileAttestationTrustStore attestationTrustStore{ paaTrustStorePath };

if (attestationTrustStore.IsInitialized())
{
return &attestationTrustStore;
}
else
{
return nullptr;
}
}
} // namespace

CHIP_ERROR CHIPCommand::Run()
{
StartTracing();
Expand All @@ -58,8 +75,17 @@ CHIP_ERROR CHIPCommand::Run()
factoryInitParams.listenPort = port;
ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().Init(factoryInitParams));

// TODO(issue #15209): Replace this trust store with file-based trust store
const chip::Credentials::AttestationTrustStore * trustStore = chip::Credentials::GetTestAttestationTrustStore();
const chip::Credentials::AttestationTrustStore * trustStore =
GetTestFileAttestationTrustStore(mPaaTrustStorePath.HasValue() ? mPaaTrustStorePath.Value() : ".");
if (trustStore == nullptr)
{
ChipLogError(chipTool, "No PAAs found in path: %s", mPaaTrustStorePath.HasValue() ? mPaaTrustStorePath.Value() : ".");
ChipLogError(chipTool,
"Please specify a valid path containing trusted PAA certificates using [--paa-trust-store-path paa/file/path] "
"argument");

return CHIP_ERROR_INVALID_ARGUMENT;
}

ReturnLogErrorOnFailure(InitializeCommissioner(kIdentityNull, kIdentityNullFabricId, trustStore));
ReturnLogErrorOnFailure(InitializeCommissioner(kIdentityAlpha, kIdentityAlphaFabricId, trustStore));
Expand Down
2 changes: 2 additions & 0 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ click
# scripts/idl
lark
stringcase

cryptography
7 changes: 5 additions & 2 deletions scripts/tests/chiptest/test_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from random import randrange

TEST_NODE_ID = '0x12344321'
DEVELOPMENT_PAA_LIST = './credentials/development/paa-root-certs'


class App:
Expand Down Expand Up @@ -240,11 +241,13 @@ def Run(self, runner, apps_register, paths: ApplicationPaths):
app.start(str(randrange(1, 4096)))

runner.RunSubprocess(
tool_cmd + ['pairing', 'qrcode', TEST_NODE_ID, app.setupCode],
tool_cmd + ['pairing', 'qrcode', TEST_NODE_ID, app.setupCode] +
['--paa-trust-store-path', DEVELOPMENT_PAA_LIST],
name='PAIR', dependencies=[apps_register])

runner.RunSubprocess(
tool_cmd + ['tests', self.run_name],
tool_cmd + ['tests', self.run_name] +
['--paa-trust-store-path', DEVELOPMENT_PAA_LIST],
name='TEST', dependencies=[apps_register])

except Exception:
Expand Down
4 changes: 3 additions & 1 deletion scripts/tests/run_python_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
DEFAULT_CHIP_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..'))

MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs"


def EnqueueLogOutput(fp, tag, q):
for line in iter(fp.readline, b''):
Expand Down Expand Up @@ -88,7 +90,7 @@ def main(app: str, factoryreset: bool, app_args: str, script: str, script_args:
DumpProgramOutputToQueue(
log_cooking_threads, "\33[34mAPP \33[0m", app_process, log_queue)

script_command = ["/usr/bin/env", "python3", script,
script_command = ["/usr/bin/env", "python3", script, "--paa-trust-store-path", os.path.join(DEFAULT_CHIP_ROOT, MATTER_DEVELOPMENT_PAA_ROOT_CERTS),
'--log-format', '%(message)s'] + shlex.split(script_args)
logging.info(f"Execute: {script_command}")
test_script_process = subprocess.Popen(
Expand Down
3 changes: 3 additions & 0 deletions scripts/tools/check_includes_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
'src/app/clusters/media-playback-server/media-playback-delegate.h': {'list'},
'src/app/clusters/target-navigator-server/target-navigator-delegate.h': {'list'},

'src/credentials/attestation_verifier/FileAttestationTrustStore.h': {'vector'},
'src/credentials/attestation_verifier/FileAttestationTrustStore.cpp': {'string'},

'src/setup_payload/AdditionalDataPayload.h': {'string'},
'src/setup_payload/AdditionalDataPayloadParser.cpp': {'vector'},
'src/setup_payload/Base38Decode.h': {'string', 'vector'},
Expand Down
5 changes: 4 additions & 1 deletion src/controller/python/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ shared_library("ChipDeviceCtrl") {
]

if (chip_controller) {
public_deps += [ "${chip_root}/src/controller/data_model" ]
public_deps += [
"${chip_root}/src/controller/data_model",
"${chip_root}/src/credentials:file_attestation_trust_store",
]
} else {
public_deps += [ "$chip_data_model" ]
}
Expand Down
17 changes: 14 additions & 3 deletions src/controller/python/OpCredsBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

#include <credentials/attestation_verifier/DefaultDeviceAttestationVerifier.h>
#include <credentials/attestation_verifier/DeviceAttestationVerifier.h>
#include <credentials/attestation_verifier/FileAttestationTrustStore.h>

using namespace chip;

Expand All @@ -48,6 +49,15 @@ using Py_GenerateNOCChainFunc = void (*)(void * pyContext, const char *
using Py_SetNodeIdForNextNOCRequest = void (*)(void * pyContext, NodeId nodeId);
using Py_SetFabricIdForNextNOCRequest = void (*)(void * pyContext, FabricId fabricId);

namespace {
const chip::Credentials::AttestationTrustStore * GetTestFileAttestationTrustStore(const char * paaTrustStorePath)
{
static chip::Credentials::FileAttestationTrustStore attestationTrustStore{ paaTrustStorePath };

return &attestationTrustStore;
}
} // namespace

namespace chip {
namespace Controller {
namespace Python {
Expand Down Expand Up @@ -129,7 +139,8 @@ void * pychip_OpCreds_InitializeDelegate(void * pyContext, uint32_t fabricCreden

ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * context,
chip::Controller::DeviceCommissioner ** outDevCtrl, uint8_t fabricIndex,
FabricId fabricId, chip::NodeId nodeId, bool useTestCommissioner)
FabricId fabricId, chip::NodeId nodeId, const char * paaTrustStorePath,
bool useTestCommissioner)
{
ChipLogDetail(Controller, "Creating New Device Controller");

Expand All @@ -139,8 +150,8 @@ ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * contex
VerifyOrReturnError(devCtrl != nullptr, CHIP_ERROR_NO_MEMORY.AsInteger());

// Initialize device attestation verifier
// TODO: Replace testingRootStore with a AttestationTrustStore that has the necessary official PAA roots available
const chip::Credentials::AttestationTrustStore * testingRootStore = chip::Credentials::GetTestAttestationTrustStore();
const chip::Credentials::AttestationTrustStore * testingRootStore = GetTestFileAttestationTrustStore(
paaTrustStorePath == nullptr ? "./credentials/development/paa-root-certs" : paaTrustStorePath);
SetDeviceAttestationVerifier(GetDefaultDACVerifier(testingRootStore));

chip::Crypto::P256Keypair ephemeralKey;
Expand Down
3 changes: 2 additions & 1 deletion src/controller/python/chip-device-ctrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def __init__(self, rendezvousAddr=None, controllerNodeId=1, bluetoothAdapter=Non
self.chipStack = ChipStack.ChipStack(
bluetoothAdapter=bluetoothAdapter, persistentStoragePath='/tmp/chip-device-ctrl-storage.json')
self.fabricAdmin = FabricAdmin.FabricAdmin()
self.devCtrl = self.fabricAdmin.NewController(controllerNodeId, True)
self.devCtrl = self.fabricAdmin.NewController(
nodeId=controllerNodeId, useTestCommissioner=True)

self.commissionableNodeCtrl = ChipCommissionableNodeCtrl.ChipCommissionableNodeController(
self.chipStack)
Expand Down
4 changes: 2 additions & 2 deletions src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class DCState(enum.IntEnum):
class ChipDeviceController():
activeList = set()

def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, fabricIndex: int, nodeId: int, useTestCommissioner: bool = False):
def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, fabricIndex: int, nodeId: int, paaTrustStorePath: str = "", useTestCommissioner: bool = False):
self.state = DCState.NOT_INITIALIZED
self.devCtrl = None
self._ChipStack = builtins.chipStack
Expand All @@ -96,7 +96,7 @@ def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, fabricIndex:

res = self._ChipStack.Call(
lambda: self._dmLib.pychip_OpCreds_AllocateController(ctypes.c_void_p(
opCredsContext), pointer(devCtrl), fabricIndex, fabricId, nodeId, useTestCommissioner)
opCredsContext), pointer(devCtrl), fabricIndex, fabricId, nodeId, ctypes.c_char_p(None if len(paaTrustStorePath) is 0 else str.encode(paaTrustStorePath)), useTestCommissioner)
)

if res != 0:
Expand Down
Loading

0 comments on commit b19fcd7

Please sign in to comment.