Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

owner certificate should be CMS struct #88

Closed
ashwinkp8 opened this issue Oct 27, 2023 · 6 comments · Fixed by #90
Closed

owner certificate should be CMS struct #88

ashwinkp8 opened this issue Oct 27, 2023 · 6 comments · Fixed by #90
Assignees

Comments

@ashwinkp8
Copy link

as per the sztpd spec the owner certificate is a CMS structure specified by RFC5652. the owner certificate generated using the generate tool does not have follow the RFC. it does not have all the elements of the CMS struct

@gmacf
Copy link
Contributor

gmacf commented Oct 30, 2023

Hi Ashwin, could you clarify which elements are missing from the Ownership Certificate?

Also please note the generate.go file is only used to generate testdata. Real Ownership Certificates and Vouchers are expected to contain the full structure.

@gmacf gmacf self-assigned this Oct 30, 2023
@ashwinkp8
Copy link
Author

Hi

Thanks for the update.
I understand this is a test data. but would be nice if it can conform to standards. All of the other data meet the standards except for this one.

I used the following python code to get the CMS data and X509 data

import base64
import os

from asn1crypto import cms
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import pkcs7
from cryptography.x509 import load_pem_x509_certificate

home_dir = os.getenv("HOME")
dir_path = f"{home_dir}/tmp/ws/bootz/testdata"
oc_pem_file = "oc_pub.pem"
ov_pem_file = "ov_123A.txt"


def print_pkcs7_cert_chain_data(p7data_bytes):
    cert_chain = pkcs7.load_der_pkcs7_certificates(p7data_bytes)
    print(f"# of certs found: {len(cert_chain)}")
    # for cert in cert_chain:
    #     print_cert_data(cert)


def print_cert_data(cert):
    print(cert)
    print(f"issuer:     {cert.issuer}")
    print(f"subject:    {cert.subject}")
    print(f"valid from: {cert.not_valid_before}")
    print(f"valid till: {cert.not_valid_after}")


def get_pkcs7_content(file_name: str):
    print("=======================================================================")
    print(f"attempting to parse the content of file {file_name} as CMS struct")
    try:
        file_path = f"{dir_path}/{file_name}"
        p7data_str = open(file_path, "r").read()
        p7data_str = p7data_str.replace("-----BEGIN CERTIFICATE-----", "")
        p7data_str = p7data_str.replace("-----END CERTIFICATE-----", "")
        p7data_bytes = base64.b64decode(p7data_str)
        print_pkcs7_cert_chain_data(p7data_bytes)
        ci = cms.ContentInfo.load(p7data_bytes)
        print(f"CMS ContentInfo Object: {ci}")
        print(f"CMS Content Type:       {ci['content_type']}")
    except Exception as e:
        print(e)


def get_x509_content(file_name: str):
    print("-----------------------------------------------------------------------")
    try:
        print(f"attempting to parse the content of file {file_name} as PEM certificate")
        file_path = f"{dir_path}/{file_name}"
        p7data_str = open(file_path, "rb").read()
        cert = load_pem_x509_certificate(p7data_str, default_backend())
        print_cert_data(cert)
    except Exception as e:
        print(e)


if __name__ == "__main__":
    get_pkcs7_content(ov_pem_file) # works
    get_x509_content(ov_pem_file)  # fails
    get_pkcs7_content(oc_pem_file) # fails
    get_x509_content(oc_pem_file)  # works

The following is the output.

python code.py 
=======================================================================
attempting to parse the content of file ov_123A.txt as CMS struct
# of certs found: 1
CMS ContentInfo Object: <asn1crypto.cms.ContentInfo 140535731450112 b'0\x82\x11\xa3\x06\t*\x86H\x86\xf7\r\x01\x07\x02\xa0\x82\x11\x940\x82\x11\x90\x02\x01\x011\r0\x0b\x06\t`\x86H\x01e\x03\x04\x02\x010\x82\x08\xc0\x06\t*\x86H\x86\xf7\r\x01\x07\x01\xa0\x82\x08\xb1\x04\x82\x08\xad{"ietf-voucher:voucher":{"created-on":"2023-10-30 05:34:36.840703127 +0000 UTC m=+2.841239642","expires-on":"2024-10-29 05:34:36.840703127 +0000 UTC m=+31536002.841239642","serial-number":"123A","assertion":"","pinned-domain-cert":"MIIFrzCCA5egAwIBAgICB+cwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQKEwZHb29nbGUxGTAXBgNVBAMTEERldmljZSBPd25lciBQREMwHhcNMjMxMDMwMDUzNDM0WhcNMzMxMDMwMDUzNDM0WjBeMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxDzANBgNVBAoTBkdvb2dsZTEZMBcGA1UEAxMQRGV2aWNlIE93bmVyIFBEQzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOSgItU53oDDo1ibp4CjHDqzmHEEUD9463CkThN05XEH3VAscjleC/2X+hmqnp4ttV5os/aV0eUJnjufOd3eRVJXF1YMb52duWol+7xk8Lb5iS8gBXsQDIu0754gDRiB5ohobJeWHTzAJ+gHdiviHp9OIhQgCZdJ+ticBvXAl/pyJk5Ng/Gm6ITxd56/nIC37Om3+S18veZK7pcQJ/b8ukLD6VJEixXEr4g1CGzdIdCGHL+5OwfyRpFGNEL9P5jV8hC5Tc+CDmT4QbB+z4I7oMrtGzUZozUX/3gzqTKiXpkbOmxB8I1sS683t1dEru/uqwQzN6rUIvZjO+jT1wIW3Lwfvd++zl/DNCC1fM0hSWKwdg4dTTxS5CMWsuDpVKqgjdfoGEYBfir6/o9xna3ipiF47TExUZ9m70tK7dSMRjZLjSVdq+kwQgm5R/Y3d2t2jv5T/l9IEohUFKMvI61TOlE3KOtbmkGUY2cGg4hfGnEDXF7Dpgrn40bvBY+5WcM4uTH8ZRBYU+lFK6XjgHsMKKUv8SZWOKjtCnIk5GAnZSDzfpB3uXsaXECCeMBE7Vn2btuBIOCVLIj04Ksl7ZxVZgNql6IpQ0S/Zh6xchB4jsVgWXMduFMDUM5CWHkrIqtKdw7MB3tFDgz3qOdMZsz/6PrHBSs9Du10pbWaa+MGKsoTAgMBAAGjdzB1MA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUcJXIhLivm+bUzOHOYHo0em/AMGkwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4ICAQBObevRa+1fRLG5dXx7IfA7thC0aJ5muqDYhL8DNF6kfVEZDmTBgsbi2H5h4PdWTt9VQTpuT6aG8DY0S7+cmvf/E227ovJjcVAOo3L89nhPIoc40190y+pa7qxtAxsMmf/6wKifdiQOuJD6gJWyOvi/QM4yEaHUQLsXA851AdXdrgDpFdDV3BHQmTw7IGB/L2i+r+gLDWqJ4I+gcLq0bowXJ+sbH9fQpHj6X7qUHewJQGUfnfy9jc5c0yNxgsp3HSkQpmdgdSm/NJKdmk+IhGAe/yPfvXsSKC+Cd4Ko4bU4NFOyvu50gC9oN0kE+1tLaa40MnINAJzvogWPPexXS+/0GtFNJfbjZRU3BALQGkoQk1NC3PTI66r1yoQs7YDpClGsvcXWYgZ5F7Uzm7/QKqUNMjfse8xytN6rCLo4NqCg3kv/GDP9ZLu/tOsykqUX08VUqo3IJ6hO1yAhCSMlJ0EDiN52tprRyOj8xcrkfjlU08JKKYirftJKX5qwjyX1Whx8JMDG3tXEy3WY88AZk/XEdP7qn1IhqLjXcHwGJJzA4UK7qi+JhQLqCLDxHnJpMzBUSVvc7qf870Uhpi+qt1FDBomZURAgA2mBOdu9waWQE9hStkvqGff+0V2tdtH7tkpuYE8ONtY9aG0b5c6bULkl625+krGv4rTyllnT7V+jYQ==","domain-cert-revocation-checks":false}}\xa0\x82\x05\xb90\x82\x05\xb50\x82\x03\x9d\xa0\x03\x02\x01\x02\x02\x02\x07\xe70\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000a1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x0b0\t\x06\x03U\x04\x08\x13\x02CA1\x160\x14\x06\x03U\x04\x07\x13\rMountain View1\x0e0\x0c\x06\x03U\x04\n\x13\x05Cisco1\x1d0\x1b\x06\x03U\x04\x03\x13\x14Manufacturer Root CA0\x1e\x17\r231030053434Z\x17\r331030053434Z0a1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x0b0\t\x06\x03U\x04\x08\x13\x02CA1\x160\x14\x06\x03U\x04\x07\x13\rMountain View1\x0e0\x0c\x06\x03U\x04\n\x13\x05Cisco1\x1d0\x1b\x06\x03U\x04\x03\x13\x14Manufacturer Root CA0\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xd1:5\xd2>\xc3b\x96\xa6\xbbb\xf4\xc1\x7f{\xc7`\x1b\xe1k\xfa\xec+\x03YcyP\xbe\xe2D/\x9a\x9cT\xaa\xd4L8t\x1a\xb7\x99\x82\xb1\xea\xf2,\x83\x1b\xae\xe1\xd1\x96\x0eS\xb5\xa3:\xdd8\xbc\xfb\xe1\x02\xb6\xe1?\x97\xe2z\xefG\xba\xa9W9;\x97q\nE\x1a\xdf\xf3\xbd\xe0\xbe\x96\xde\xd2+\xc1\x1f\x169\x03\x0bd6\xe2\x95\xd3\xde/[lG\x94\xa5\xcc\xabM\x05\x8eL\x8eXh\x9a\xe4\xf8q\x19\x8b\xedG\xbf_\xea\xbe\xdc\xf9\x7f\x08\xed\x95\xdd\x0eN\xc8&\x89\xe5hB\xb9\xc2(\xe0w5#\x00\\i\x7f\xdaU\r~\x9e\xb6u!o\x1c\xf2\xbd\x1eG\xa5g\x12\x18\x82\xfdgE\x07\xee#\xbe\xc2\xc0\xe8\x80\x8d\xed&\xffoz\x104\x7fg\xb7G\xa6\xeb\xc6\xb4\x85C\x92\xe2O\xbfS\xba4\xfb\xa3\xae\x94\xf1|\xf7D\x94\xf2M\x9a\x16\x16\xf1\xbek\x99\x15\xb5\xbb<\x01\xe3\xba\xdcY\x9e_/\xbc\x86Z\x19\xb9\x85;\x1d\x06\x050\xa0\x00n\x98\x14p\x16\x06+.\x13\xe3\xf5\x0b\x19ClfI\x80\x84Fwy!\xef*B\xe0\xe3\x95\x1e\'\xf7F\x8bS\x15\xe5\xa90\xdd/\x85\x98+\x15\xb0r\x87/@\xdbl\'s\xd6Q\xbf\xe3\xfa\xf4\xcda\xd5\xec\xd2\x9c\x9eB\xbac\xf2\x950\xc3\xc1lj\xe4G\xe9\x18\xc9@\x17\xb2\x08\xdcQ\xb1\xbf\xbb\x98u\x87\xc7n\x80\xd3\x9car\xaf\x83_\x8e1)\x14\x97\xea\xc2\xd1\xa1\x05\x11"\x9b\x9d\xc5\xa2\xdeZ$J\x19\xbf\xa4\x0b\xa8\xf5]\x93ZQ|#6\x93z\x9b\xae\'\xea=\xfa\x1aP\xc0W\x03\xe23\xc3)\xeb\x1f\x95w\x04@\x03\xddk:\x80\xdf\x90\xe4\t4.Kg\xbc1\xb4\x16r\xe9\x0e\xd9as\x96\xd3u\xbc\xbf\x85\xcf\xd3\xbe\x93\xa9\xee\xbb\x85\x18\x9e\xf39\x9d\xc1\xf0z\xac\x9e\x92\x1a\xbd\xda\xc2\xd7\xc6\xd2\x91\x1c\x88\x98\xa6=\x85@\xc5\xf9\xfe\x85\xe0K/\x10\x7f\t\xcc\xb0\xe8\x95]ku{WX\x14\x9d\x13J\xaf\x1d\xec\tr\xc4F\xd8\xd4{\x02\x03\x01\x00\x01\xa3w0u0\x0e\x06\x03U\x1d\x0f\x01\x01\xff\x04\x04\x03\x02\x02\x840\x1d\x06\x03U\x1d%\x04\x160\x14\x06\x08+\x06\x01\x05\x05\x07\x03\x02\x06\x08+\x06\x01\x05\x05\x07\x03\x010\x0f\x06\x03U\x1d\x13\x01\x01\xff\x04\x050\x03\x01\x01\xff0\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xfa\xef\xb8\x03\xba\x8a\xd9\xd9\xfe\xd0\x88\xd8\x91\xb4l\xca\x10\x92\xddP0\x14\x06\x03U\x1d\x11\x04\r0\x0b\x82\tlocalhost0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00\x03\x82\x02\x01\x00Y\x95\xc5\xe5k\xb7]eo^/\x90O\x16T\x1c\xf3\x7f@\xda\xfbi\x08\x9c1>\r\x10\xf2u&L\xe7\x03\xaf\xd1h`9E\xd5\x8f\xc2v\xc0.\xec&G+\x8e>I\x91%A\xcb?v\xd3iH\xf1\xe4\xa8I\xdb\n\x16w\xb0N\xea\xdfbDL9\xf0\xa80$c\xe1\xae\xe4V[\xd5+T\xf0\xdc\xf3\xae\x05\xbb\xc0\x86\x91H<J\x96\xa2\xd3 \x1e\xa6\x16C0\x12\x8d\x0cq\x8e\xa0\xd7\n\xe2T\xdc\xf7\x8d\x8c%\x1b\x95\n\x84d\xc6E8\xea\xe4b\x95ZKE\x05\x8d\xd0\xd3\xf3R\x08\x91e\xf9\xa7\xd0R\xee\xc0\x8a."(\xeb\x0c\xc4q\x8a\xa1\xa0\xb4\xe3U\xeb\x8b\x1c\rrx\xa7|\xd9\xa9\xc3n\x01\x12\xdb\xefe\xa5\xc1*$"Tg\x1f\x92\xe9\xefj;\xfc$\xc2\xe8\xbb\xa1\xa4\x8eu\x91\xbb\x8c\x99\x19\x07\xe3aT\xb35\x07\xc9R\xce\xfa\xbf\xbd\x9f\xf2"\x80d:\x0f\x8f\\Y\x84\xf4\xc6In]\xb4hWh\xcd|\xc8&V\x9en\x00+!\xca)\xf5\xd5Xyyu\xee0\xdc-r\x9aT\x915\x0c\xbf\xf7\xe7S\xcf\x86\x8bc\xc5\xe4\xb9?\xc3\x8c\xb9\x1aG\x9c\xd4\xf6\xf9W\\\x9c\xa6U?\xc1\xf4%KxF5W\xe4#\xf0Qy\x85\x11\xb9\x08o8\xe0\xb6Qq\xb7K\xe0c*t\t \xed\xd1\x1d\xd8\xbb\xd7\x9dP\xcb\x18m\x1e{\x8f9\x13BjK\x91\xdf\x8e\x95\xe7_\xc1\xc0\x9b\xb6\x9c\xba`_!\x84\x95\xa0\x9c\xc7g\xb9\xc3\xd9\xbf\xd9t\x84QX\xce\x0e\x12\x12\xc8\x95\xc32\xed\xd80>\xe9yY\'\x95\xa0\xbb[#\xfc\x05\x088\xcb1\x9e\xb6M\xa6\xaf?f\xe5\xbc8\xdex\xc0\xae\xe9\xd3=\xcf\x89\x0b|\xe0\xc36\xbf\xcd\x92\xc1\xe6\x04\x8f\xa4\xec\xa2\xd2\xe1\xff=B\xab\x0b<S\x9a\x97\t\xd0\x9e\xdf\x84\xadL\xc2BJp3\x00\xe3G\x18\x97\xd6r)\xfb\xc9\xc6\xa4\x05m\xca\'0\x13\x02\xba\x0bi\x072\x18\x00\x95\x81\x11\xa5M\x11\x00 \xe1\xc4\xc5\xd6\x00\xe3\x03WX(\xb11\x82\x02\xf90\x82\x02\xf5\x02\x01\x010g0a1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x0b0\t\x06\x03U\x04\x08\x13\x02CA1\x160\x14\x06\x03U\x04\x07\x13\rMountain View1\x0e0\x0c\x06\x03U\x04\n\x13\x05Cisco1\x1d0\x1b\x06\x03U\x04\x03\x13\x14Manufacturer Root CA\x02\x02\x07\xe70\x0b\x06\t`\x86H\x01e\x03\x04\x02\x01\xa0i0\x18\x06\t*\x86H\x86\xf7\r\x01\t\x031\x0b\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1c\x06\t*\x86H\x86\xf7\r\x01\t\x051\x0f\x17\r231030053436Z0/\x06\t*\x86H\x86\xf7\r\x01\t\x041"\x04 \xb1\xe9Dz\xe4\x0c\x96\x01\x08M\xb9?\xae\x07\xf0\xdf\x8dK\xd2\x88_\xa7M\xbf\xe1\xac\x82"%s;\xce0\x0b\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x04\x82\x02\x00\x96al\x1fC\x19k\xf0\xe9\xd5%\xe4\xde\xfb\xa8\x11n!n\xac\xe7\x95\xf5S\xd0]h\xe9Lr\xf5d\xb63\x06E9\x7f\x7f\x97\xcb\xd4*k_C\xf0\xb2+\xd1S\xb5[\xb4.\xdb\xca \xdc\xce\n\xda\xc2\xeb\x0c(\xb9\xd1XS\x86\x93>\xee\xa3\xa2\xe2\xa2\xa2\x1a`d\x03\x8a\xa8\xba\x17\x9dG\xdd\xcf\x7f\xdf\x1e\xda\xabz\xa4\xbbjz\x1f\xad\x84\x9f\xe0\x88_gw\xac6\xaeYN\x8d\xc4\xe2\xc1\xba\xf8\xcc\xb3[\x8c\xb3\xc1\x83\xea\xf6\x15\xc3\xf3\xaeW\xe73\\O\xe9\xce\xf9\xe5VX\\[E\xfa\xe3\xe6\x00\x15\x92\xbf\x07\r\xc8\xd1\xb6\xfa\x1c\xb9ID\xcfx\x03\x84]\xbem\x11\xf7\xba\x10\xb2\x82Qv\xc8\x1b\x13\x82C\x00\x0c\xe8\x1a\xb7\x85\xea"\xc5S5Yh{\x14\xc8\x18\x90\xf7\xe4VU\xf5\xddz\x0b}\xc0\x0b\xb6\x9af\t\xb8\x11\x9b\x14\xa8xL\xe2\xdd\x9b\x0b\xc00H\x92*\xdb\xca\xb4\xc0\xf4U\x0e_\xdd\xfcJa\xae.\xae\xfan\xa1 ?\xd5\xf8\xc4^\x92\x89\\\xd4*$[\\\x80p[a\xd6IKI\xe9-\xd0\xb2Q\xbf\xd1\xdd\x82\xe7\x06I\xd9\x14\x9fS\xef\x85?#\xeamZ\xf5I\x99uxO\xda\x96\xe4 m\x0fz\xe8N\xae\x83\xd5)\xc8\xfd[xdH\x8dc\x91\xfb\x8a\xa4\x18I\x12.\xe0\x83\xc6\xccr\xf8\xa3\xfc\x89\xc1\xb7\xe6\x0b\x7f\x02\xbd\x11\x96\x9e\xad\x17o\x18tM\x15\xb5A5\xe2k)\x92\xc2~\xb5\x01h\x8a\xe8\x9e(\xc3;\xdb\x9fZ\xb1\x80\xa4"\x97\xb4\xcd\x11\xb2x\xfe\xa6i\xca\x87[\x15\xe1\xe7\xb4\xf0\xac\x1ao\xb4\xe6\xba\x8ec\xd0\xf4\x92\x9a\xba\x96IM\xcc\xd9\xc2\xa7\x14\xac\xeeM\x9cp\xf5\x1a\x9d\x86\x1d\xaa\xa7V]\xb5\x8ax\'\\\x1e\x00"\x12\xf7\xd0\x8e\xf6kB\x91\x165\x81\x15\x99x{"S\x04\x87y\xd3\x7f\x17\x11\xa4k\rW\x81\x04\x0f\xd1t;\x07\xa8\x07\xf8\xc5e\xa0\xe6^!\xdb\x8a\x06\xde\xa3\x16@]\xb8U\xb5\xe4\x18\xff\xc3I\xe9B^.eN^\x06\x1c'>
CMS Content Type:       1.2.840.113549.1.7.2
-----------------------------------------------------------------------
attempting to parse the content of file ov_123A.txt as PEM certificate
Unable to load PEM file. See https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file for more details. MalformedFraming
=======================================================================
attempting to parse the content of file oc_pub.pem as CMS struct
Unable to parse PKCS7 data
-----------------------------------------------------------------------
attempting to parse the content of file oc_pub.pem as PEM certificate
<Certificate(subject=<Name(C=US,ST=CA,L=Mountain View,O=Google,CN=Device Owner PDC)>, ...)>
issuer:     <Name(C=US,ST=CA,L=Mountain View,O=Google,CN=Device Owner PDC)>
subject:    <Name(C=US,ST=CA,L=Mountain View,O=Google,CN=Device Owner PDC)>
valid from: 2023-10-31 02:23:48
valid till: 2033-10-31 02:23:48

Process finished with exit code 0

@gmacf
Copy link
Contributor

gmacf commented Oct 31, 2023

Thanks for clarifying. I see now that we shouldn't be returning the PEM encoding of the OC but rather return it as a CMS struct. I will create a PR for that.

@gmacf gmacf linked a pull request Oct 31, 2023 that will close this issue
@gmacf
Copy link
Contributor

gmacf commented Oct 31, 2023

@ashwinkp8 Please take a look at the PR when you get a chance and let me know if I've missed anything.

@ashwinkp8
Copy link
Author

Hi Gareth

Thanks. The PR verification was OK.

Ashwini Kumar

@gmacf gmacf closed this as completed in #90 Nov 1, 2023
@kattasrao
Copy link

Hi,
What is done to encode owner certificate as CMS seem to be uncommon.
I don't see why owner certificate should be signed using owner-private-key to convert into CMS format.

PKCS#7 and CMS which is ++version of PKCS#7, support "degenerate form" which can be used to transport certificates
without signers.

You can also check section 3.2 of RFC 8572 on how owner certificate is encoded in Secure-ZTP. Same can be done here.

Also check:

https://www.openssl.org/docs/manmaster/man1/openssl-crl2pkcs7.html

Thanks,
Katta

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants