diff --git a/cmp/bouncycastle/build.gradle.kts b/cmp/bouncycastle/build.gradle.kts index 28b052b2..283fc8fb 100644 --- a/cmp/bouncycastle/build.gradle.kts +++ b/cmp/bouncycastle/build.gradle.kts @@ -10,12 +10,7 @@ repositories { } dependencies { -// testImplementation(platform("org.junit:junit-bom:5.9.1")) -// testImplementation("org.junit.jupiter:junit-jupiter") implementation("org.bouncycastle:bcprov-jdk18on:1.73") implementation("org.bouncycastle:bcpkix-jdk18on:1.73") + implementation("net.sourceforge.argparse4j:argparse4j:0.9.0") } - -tasks.test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/cmp/bouncycastle/readme.md b/cmp/bouncycastle/readme.md index fbc8c5fd..a30c2999 100644 --- a/cmp/bouncycastle/readme.md +++ b/cmp/bouncycastle/readme.md @@ -14,4 +14,12 @@ Both outputs are saved to files, located in the same directory as the given inpu # Usage Run `Main.java` with a command line argument that points to the directory where the OpenSSL-generated artifacts are. -For example, the files in `../oqs-openssl/artifacts` are suitable for this purpose. \ No newline at end of file +For example, the files in `../oqs-openssl/artifacts` are suitable for this purpose. + +## As a CMP server + +An alternative entry point is `MainCmpHttp.java` - this is a simple HTTP server that wraps the CMP logic, which you +can use as a CA that will issue certificates signed with post-quantum algorithms. + +Once the server is started, you can send one of the pre-generated payloads (e.g., from oqs-openssl/artifacts) to give +it a try: `curl -X POST --data-binary @req-ir-prot_none-pop_sig.pkimessage http://127.0.0.1:8000/pkix --header "Content-Type:application/pkixcmp" -o response.bin` diff --git a/cmp/bouncycastle/src/main/java/org/example/CmpLogic.java b/cmp/bouncycastle/src/main/java/org/example/CmpLogic.java new file mode 100644 index 00000000..914bdf64 --- /dev/null +++ b/cmp/bouncycastle/src/main/java/org/example/CmpLogic.java @@ -0,0 +1,219 @@ +/* +* Basic example of CMP request processing with post-quantum signature algorithms, derived from prototype built at +* the IETF116 hackathon. +* */ + +package org.example; + +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIMessage; +import org.bouncycastle.asn1.cmp.PKIStatus; +import org.bouncycastle.asn1.cmp.PKIStatusInfo; +import org.bouncycastle.asn1.crmf.CertTemplate; +import org.bouncycastle.asn1.pkcs.CertificationRequest; +import org.bouncycastle.asn1.pkcs.CertificationRequestInfo; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.cert.CertException; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.cmp.ProtectedPKIMessage; +import org.bouncycastle.cert.cmp.ProtectedPKIMessageBuilder; +import org.bouncycastle.cert.crmf.*; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.*; +import org.bouncycastle.util.io.Streams; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Date; +import java.util.Map; +import java.util.logging.Logger; + +import static java.util.Map.entry; + + +public class CmpLogic { + private static final Logger log = Logger.getLogger("cmp"); + private static GeneralName issuerName = null; + private static String signAlgorithmName = null; + + private static ContentSigner signer; + + private static X509CertificateHolder caCert; + + // parameters are hardcoded for now; only tested with a few algorithms - others need to be reviewed + private static final Map keyParameters = Map.ofEntries( + entry("Dilithium", DilithiumParameterSpec.dilithium2), +// entry("Kyber", KyberParameterSpec.kyber512), +// entry("NTRU", NTRUParameterSpec.ntruhps2048509), +// entry("Falcon", FalconParameterSpec.falcon_512), +// entry("NTRUPrime", SNTRUPrimeParameterSpec.sntrup653), +// entry("Rainbow", RainbowParameterSpec.rainbowIIIclassic), + entry("Picnic", PicnicParameterSpec.picnic3l1), +// entry("BIKE", BIKEParameterSpec.bike128), +// entry("HQC", HQCParameterSpec.hqc128), + entry("XMSS", XMSSParameterSpec.SHA2_10_256), +// entry("Frodo", FrodoParameterSpec.frodokem640aes), +// entry("CMCE", CMCEParameterSpec.mceliece348864), + entry("SPHINCSPlus", SPHINCSPlusParameterSpec.haraka_128f) + +// entry("NH", ), +// entry("SPHINCS", SPHINCSKeyParameters.SHA3_256), +// entry("LMS", LMS, -> this requires more effort, e.g. handling the key index +// entry("SABER", SABERParameterSpec.), + ); + + public static void init(String algorithm) throws Exception { + // initialize BouncyCastle Providers for classic and post-quantum crypto + Security.addProvider(new BouncyCastlePQCProvider()); + Security.addProvider(new BouncyCastleProvider()); + + signAlgorithmName = algorithm; + String rawIssuerName = String.format("CN=%s Issuer", algorithm); + issuerName = new GeneralName(new X500Name(rawIssuerName)); + + // generate key-pairs + log.info("Generating key pair for algorithm: " + signAlgorithmName); + KeyPairGenerator kpGen = KeyPairGenerator.getInstance(signAlgorithmName, "BCPQC"); + + AlgorithmParameterSpec algorithmParameters = keyParameters.get(algorithm); + kpGen.initialize(algorithmParameters); + KeyPair issuerKeypair = kpGen.generateKeyPair(); + + signer = new JcaContentSignerBuilder(signAlgorithmName).setProvider("BCPQC").build(issuerKeypair.getPrivate()); + + log.info("Creating issuer certifcate for: " + rawIssuerName); + caCert = makeV3Certificate(rawIssuerName, issuerKeypair); + + } + + public static void processCmpRequest(Path path) throws Exception { + log.info("Processing " + path.toString()); + byte[] data = Streams.readAll(new FileInputStream(path.toString())); + + ProtectedPKIMessage pkiMessage = issueCertificate(data); + if (pkiMessage == null) return; + String signAlgorithmOid = signer.getAlgorithmIdentifier().getAlgorithm().getId(); + + // req-ir-prot_none-pop_sig.pkimessage + Path originalName = path.getFileName(); + String modifiedName = originalName.toString().replace("req-", "resp-"); + String signatureNameSuffix = String.format("-sig_%s_%s.pkimessage", signAlgorithmOid, signAlgorithmName); + modifiedName = modifiedName.replace(".pkimessage", signatureNameSuffix); + Path parentDir = path.getParent(); + + // Write the PKIMessage answer that would've been sent to the client + FileOutputStream resp = new FileOutputStream(parentDir.resolve(modifiedName).toString()); + resp.write(pkiMessage.toASN1Structure().getEncoded()); + resp.close(); + + // take certificate from response + CertificateRepMessage certRepMessage = CertificateRepMessage.fromPKIBody(pkiMessage.getBody()); + CertificateResponse certResp = certRepMessage.getResponses()[0]; + Certificate cert = certResp.getCertificate().getX509v3PKCert(); + + String certName = String.format("issued-cert-sig_%s.crt", signAlgorithmName); + + + FileOutputStream certWriter = new FileOutputStream(parentDir.resolve(certName).toString()); + certWriter.write(cert.getEncoded()); + certWriter.close(); + } + + // Used for issuing the self-signed certificate + private static X509CertificateHolder makeV3Certificate(String _subDN, KeyPair issKP) + throws OperatorCreationException, CertException, CertIOException { + X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder( + new X500Name(_subDN), + BigInteger.valueOf(System.currentTimeMillis()), + new Date(System.currentTimeMillis()), + new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), + new X500Name(_subDN), + issKP.getPublic()); + + certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); + + X509CertificateHolder certHolder = certGen.build(signer); + return certHolder; + } + + private static X509CertificateHolder makeV3Certificate(SubjectPublicKeyInfo pubKey, X500Name _subDN, String _issDN) + throws OperatorCreationException, CertException, CertIOException { + + log.info("Issuing cert for: " + _subDN); + X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder( + new X500Name(_issDN), + BigInteger.valueOf(System.currentTimeMillis()), + new Date(System.currentTimeMillis()), + new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), + _subDN, + pubKey); + + certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + X509CertificateHolder certHolder = certGen.build(signer); + return certHolder; + } + + public static ProtectedPKIMessage issueCertificate(byte[] rawRequest) + throws Exception { + PKIMessage initMessage = PKIMessage.getInstance(rawRequest); + + + X509CertificateHolder cert; + X500Name subject = null; + ASN1Integer requestId = new ASN1Integer(1); + + // TODO: check the signature before issuing the cert + + if (initMessage.getBody().getType() == PKIBody.TYPE_P10_CERT_REQ) { + CertificationRequest csr = (CertificationRequest) initMessage.getBody().getContent(); + CertificationRequestInfo requestInfo = csr.getCertificationRequestInfo(); + subject = requestInfo.getSubject(); + cert = makeV3Certificate(requestInfo.getSubjectPublicKeyInfo(), subject, issuerName.getName().toString()); + + } else if (initMessage.getBody().getType() == PKIBody.TYPE_INIT_REQ) { + CertificateReqMessages requestMessages = CertificateReqMessages.fromPKIBody(initMessage.getBody()); + // TODO consider adding test cases of inputs with multiple requests in a payload + CertificateRequestMessage senderReqMessage = requestMessages.getRequests()[0]; + CertTemplate certTemplate = senderReqMessage.getCertTemplate(); + subject = certTemplate.getSubject(); + + requestId = senderReqMessage.getCertReqId(); + + cert = makeV3Certificate(certTemplate.getPublicKey(), certTemplate.getSubject(), issuerName.getName().toString()); + } else { + log.warning("Ignoring unsupported request type " + initMessage.getBody().getType()); + return null; + } + + // response generation + CertificateResponseBuilder certRespBuilder = new CertificateResponseBuilder(requestId, new PKIStatusInfo(PKIStatus.granted)); + certRespBuilder.withCertificate(cert); + CertificateRepMessageBuilder repMessageBuilder = new CertificateRepMessageBuilder(caCert); + repMessageBuilder.addCertificateResponse(certRespBuilder.build()); + + // sign response + CertificateRepMessage repMessage = repMessageBuilder.build(); + GeneralName sender = new GeneralName(subject); + + return new ProtectedPKIMessageBuilder(sender, issuerName) + .setBody(PKIBody.TYPE_CERT_REP, repMessage) + .build(signer); + } + +} diff --git a/cmp/bouncycastle/src/main/java/org/example/Main.java b/cmp/bouncycastle/src/main/java/org/example/Main.java index bdc48d5e..b8ed8db0 100644 --- a/cmp/bouncycastle/src/main/java/org/example/Main.java +++ b/cmp/bouncycastle/src/main/java/org/example/Main.java @@ -1,63 +1,48 @@ package org.example; -import org.bouncycastle.asn1.ASN1Integer; -import org.bouncycastle.asn1.cmp.PKIBody; -import org.bouncycastle.asn1.cmp.PKIMessage; -import org.bouncycastle.asn1.cmp.PKIStatus; -import org.bouncycastle.asn1.cmp.PKIStatusInfo; -import org.bouncycastle.asn1.crmf.CertTemplate; -import org.bouncycastle.asn1.pkcs.CertificationRequest; -import org.bouncycastle.asn1.pkcs.CertificationRequestInfo; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.*; -import org.bouncycastle.cert.CertException; -import org.bouncycastle.cert.CertIOException; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.cert.cmp.ProtectedPKIMessage; -import org.bouncycastle.cert.cmp.ProtectedPKIMessageBuilder; -import org.bouncycastle.cert.crmf.*; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; -import org.bouncycastle.pqc.jcajce.spec.DilithiumParameterSpec; -import org.bouncycastle.util.io.Streams; +import net.sourceforge.argparse4j.ArgumentParsers; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.Namespace; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.Security; -import java.util.Date; import java.util.List; import java.util.stream.Stream; - +import java.util.logging.Logger; public class Main { - static GeneralName issuerName = new GeneralName(new X500Name("CN=Dilithium Issuer")); - static String SignAlgorithmName = "Dilithium"; + private static final Logger log = Logger.getLogger("main"); + // taken from https://github.com/bcgit/bc-java/blob/master/prov/src/main/java/org/bouncycastle/pqc/jcajce/provider/BouncyCastlePQCProvider.java#L38 + // and removed some entries that didn't work in practice + // NOTE: these are still not tested: "LMS", "NH", "CMCE", "Frodo", "SABER", "NTRU", "NTRUPrime", "BIKE", "HQC", "Rainbow", + // "Kyber","Falcon", + static String[] supportedAlgorithms = {"Dilithium", "XMSS", "SPHINCSPlus", "Picnic"}; + public static void main(String[] args) throws Exception { - private static KeyPair dilKp; // issuer's key + ArgumentParser parser = ArgumentParsers.newFor("Main").build() + .defaultHelp(true) + .description("Process CMP requests"); - private static ContentSigner signer; + parser.addArgument("input") + .help("Path for the input directory with CMP requests") + .type(String.class); - private static X509CertificateHolder caCert; + parser.addArgument("-a", "--algorithm") + .help("Algorithm to use when issuing a certificate") + .choices(supportedAlgorithms).setDefault("Dilithium"); - public static void main(String[] args) throws Exception { - if (args.length < 1) { - System.out.println("You must provide a path for the input directory"); + Namespace res = null; + try { + res = parser.parseArgs(args); + } catch (ArgumentParserException e) { + parser.handleError(e); return; } - Path sourcePath = Paths.get(args[0]); + Path sourcePath = Paths.get(res.getString("input")); if (!Files.isDirectory(sourcePath)) { throw new IllegalArgumentException("Path must be a directory!"); } @@ -77,137 +62,16 @@ public static void main(String[] args) throws Exception { throw new IllegalArgumentException("Path does not seem to contain any CMP requests"); } - // initialize BouncyCastle Providers for classic and post-quantum crypto - Security.addProvider(new BouncyCastlePQCProvider()); - Security.addProvider(new BouncyCastleProvider()); - - - // generate key-pairs - KeyPairGenerator dilKpGen = KeyPairGenerator.getInstance(SignAlgorithmName, "BCPQC"); - dilKpGen.initialize(DilithiumParameterSpec.dilithium2); - dilKp = dilKpGen.generateKeyPair(); + CmpLogic.init(res.getString("algorithm")); - signer = new JcaContentSignerBuilder(SignAlgorithmName).setProvider("BCPQC").build(dilKp.getPrivate()); - - caCert = makeV3Certificate("CN=Dilithium Issuer", dilKp); int filesProcessed = 0; for (Path requestPath : requestPaths) { - processCmpRequest(requestPath); + CmpLogic.processCmpRequest(requestPath); filesProcessed++; } - System.out.printf("Done! %d files", filesProcessed); - } - - private static void processCmpRequest(Path path) throws Exception { - System.out.printf("Processing %s", path.toString()); - byte[] data = Streams.readAll(new FileInputStream(path.toString())); - - ProtectedPKIMessage pkiMessage = issueWithDilithiumCA(data); - String signAlgorithmOid = signer.getAlgorithmIdentifier().getAlgorithm().getId(); - - // req-ir-prot_none-pop_sig.pkimessage - Path originalName = path.getFileName(); - String modifiedName = originalName.toString().replace("req-ir", "resp-ip"); - String signatureNameSuffix = String.format("-sig_%s_%s.pkimessage", signAlgorithmOid, SignAlgorithmName); - modifiedName = modifiedName.replace(".pkimessage", signatureNameSuffix); - Path parentDir = path.getParent(); - - // Write the PKIMessage answer that would've been sent to the client - FileOutputStream resp = new FileOutputStream(parentDir.resolve(modifiedName).toString()); - resp.write(pkiMessage.toASN1Structure().getEncoded()); - resp.close(); - - // take certificate from response - CertificateRepMessage certRepMessage = CertificateRepMessage.fromPKIBody(pkiMessage.getBody()); - CertificateResponse certResp = certRepMessage.getResponses()[0]; - Certificate cert = certResp.getCertificate().getX509v3PKCert(); - - String certName = String.format("issued-cert-sig_%s.crt", SignAlgorithmName); - - - FileOutputStream certWriter = new FileOutputStream(parentDir.resolve(certName).toString()); - certWriter.write(cert.getEncoded()); - certWriter.close(); - } - - // Used for issuing the self-signed certificate - private static X509CertificateHolder makeV3Certificate(String _subDN, KeyPair issKP) - throws OperatorCreationException, CertException, CertIOException { - X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder( - new X500Name(_subDN), - BigInteger.valueOf(System.currentTimeMillis()), - new Date(System.currentTimeMillis()), - new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), - new X500Name(_subDN), - issKP.getPublic()); - - certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); - - X509CertificateHolder certHolder = certGen.build(signer); - return certHolder; - } - - private static X509CertificateHolder makeV3Certificate(SubjectPublicKeyInfo pubKey, X500Name _subDN, String _issDN) - throws OperatorCreationException, CertException, CertIOException { - - X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder( - new X500Name(_issDN), - BigInteger.valueOf(System.currentTimeMillis()), - new Date(System.currentTimeMillis()), - new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), - _subDN, - pubKey); - - certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); - X509CertificateHolder certHolder = certGen.build(signer); - return certHolder; - } - - public static ProtectedPKIMessage issueWithDilithiumCA(byte[] rawRequest) - throws Exception { - PKIMessage initMessage = PKIMessage.getInstance(rawRequest); - - - X509CertificateHolder cert; - X500Name subject = null; - ASN1Integer requestId = new ASN1Integer(1); - - // TODO: check the signature before issuing the cert - - if (initMessage.getBody().getType() == PKIBody.TYPE_P10_CERT_REQ) { - CertificationRequest csr = (CertificationRequest) initMessage.getBody().getContent(); - CertificationRequestInfo requestInfo = csr.getCertificationRequestInfo(); - subject = requestInfo.getSubject(); - cert = makeV3Certificate(requestInfo.getSubjectPublicKeyInfo(), subject, issuerName.getName().toString()); - - } else { - - CertificateReqMessages requestMessages = CertificateReqMessages.fromPKIBody(initMessage.getBody()); - // TODO consider adding test cases of inputs with multiple requests in a payload - CertificateRequestMessage senderReqMessage = requestMessages.getRequests()[0]; - CertTemplate certTemplate = senderReqMessage.getCertTemplate(); - subject = certTemplate.getSubject(); - - requestId = senderReqMessage.getCertReqId(); - - cert = makeV3Certificate(certTemplate.getPublicKey(), certTemplate.getSubject(), issuerName.getName().toString()); - } - - // response generation - CertificateResponseBuilder certRespBuilder = new CertificateResponseBuilder(requestId, new PKIStatusInfo(PKIStatus.granted)); - certRespBuilder.withCertificate(cert); - CertificateRepMessageBuilder repMessageBuilder = new CertificateRepMessageBuilder(caCert); - repMessageBuilder.addCertificateResponse(certRespBuilder.build()); - - // sign response - CertificateRepMessage repMessage = repMessageBuilder.build(); - GeneralName sender = new GeneralName(subject); - - return new ProtectedPKIMessageBuilder(sender, issuerName) - .setBody(PKIBody.TYPE_CERT_REP, repMessage) - .build(signer); + log.info("Done! Processed files: " + filesProcessed); } } diff --git a/cmp/bouncycastle/src/main/java/org/example/MainCmpHttp.java b/cmp/bouncycastle/src/main/java/org/example/MainCmpHttp.java new file mode 100644 index 00000000..a64348a1 --- /dev/null +++ b/cmp/bouncycastle/src/main/java/org/example/MainCmpHttp.java @@ -0,0 +1,86 @@ +/* +* This is a simple HTTP wrapper around the CMP logic. It exposes an endpoint at /pkix, where it expects CMP requests +* to arrive in the form described in RFC 6712. +* It is not production-ready, it is not scalable, it is only suitable for a development server to test against. +* */ + +package org.example; + +import java.io.*; +import java.net.InetSocketAddress; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.bouncycastle.cert.cmp.ProtectedPKIMessage; + +public class MainCmpHttp { + public static void main(String[] args) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); + server.createContext("/pkix", new CmpHandler()); + server.setExecutor(null); + + CmpLogic.init("Dilithium"); + System.out.println("Starting HTTP server"); + server.start(); + } + + static class CmpHandler implements HttpHandler { + public void returnError(HttpExchange t, String error) throws IOException { + t.sendResponseHeaders(400, error.getBytes().length); + OutputStream os = t.getResponseBody(); + os.write(error.getBytes()); + os.close(); + + } + + @Override + public void handle(HttpExchange t) throws IOException { + + if (! t.getRequestMethod().equalsIgnoreCase("POST")) { + returnError(t, "Only POST requests are supported"); + return; + } + + Headers requestHeaders = t.getRequestHeaders(); + + if (! "application/pkixcmp".equalsIgnoreCase(requestHeaders.getFirst("content-type"))) { + returnError(t, "Content-Type should be `application/pkixcmp`"); + return; + } + + + InputStream requestBody = t.getRequestBody(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024*640]; // ought to be enough for anybody; but think about the size of average PQ keys + int len; + while ((len = requestBody.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + byte[] requestBodyBytes = baos.toByteArray(); + System.out.println("Got request: " + requestBodyBytes.length); + try { + ProtectedPKIMessage cmpResponse = CmpLogic.issueCertificate(requestBodyBytes); + System.out.println("Issued cert"); + // Send response headers + Headers responseHeaders = t.getResponseHeaders(); + responseHeaders.set("Content-Type", "application/pkixcmp"); + t.sendResponseHeaders(200, 0); + + byte[] encodedResponse = cmpResponse.toASN1Structure().getEncoded(); + // Send response body + OutputStream responseBody = t.getResponseBody(); + responseBody.write(encodedResponse); + responseBody.close(); + + System.out.println("Sent response: " + encodedResponse.length); + } catch (Exception e) { + returnError(t, "Could not parse your payload, is it a valid PKIMessage?"); + throw new RuntimeException(e); + } + System.out.println("Done!"); + } + } + +} \ No newline at end of file diff --git a/cmp/oqs-openssl/artifacts-qsckeys.zip b/cmp/oqs-openssl/artifacts-qsckeys.zip deleted file mode 100644 index 2a68d68d..00000000 Binary files a/cmp/oqs-openssl/artifacts-qsckeys.zip and /dev/null differ diff --git a/cmp/oqs-openssl/gen.py b/cmp/oqs-openssl/gen.py index 31fbc5b3..d1743a08 100644 --- a/cmp/oqs-openssl/gen.py +++ b/cmp/oqs-openssl/gen.py @@ -16,6 +16,7 @@ import logging from itertools import product import argparse +from enum import Enum, auto from algorithm_oids import ALG_OID @@ -84,6 +85,9 @@ } POP_INVERTED = {v: k for k, v in POP.items()} +# path to pre-generated keys and certificates needed when "signature-based protection" is required. +DIR_PROTECTION_DATA = '/protection-data/' + def run_command(command): LOG.info(f'Running: `{COMMAND_PREFIX} {command}`') @@ -111,7 +115,7 @@ def command_generate_keypair(algorithm_name): extended_algorithm_name = f'{ALG_OID[algorithm_name]}-{algorithm_name}' extended_path = f'{OUTPUT_PATH}{extended_algorithm_name}/' - command = f'openssl genpkey -algorithm {algorithm_name} -out {extended_path}key.pem' + command = f'openssl genpkey -algorithm {algorithm_name} -out {extended_path}priv_key.pem' return run_command(command) @@ -120,13 +124,26 @@ def command_generate_csr(algorithm_name, subject="/CN=test subject"): extended_path = f'{OUTPUT_PATH}{extended_algorithm_name}/' command = f'openssl req -out {extended_path}csr.pem -new ' \ - f'-key {extended_path}key.pem ' \ + f'-key {extended_path}priv_key.pem ' \ f'-nodes -subj "{subject}"' return run_command(command) -def command_generate_cmp_ir(algorithm_name, server='127.17.0.2:8000/pkix', recipient='/CN=PQCA', reference='11111', password=None, popo=POP_SIG, cr=False): - features = 'prot_pass' if password else 'prot_none' +def command_generate_cmp_ir(algorithm_name, server='127.17.0.2:8000/pkix', recipient='/CN=PQCA', reference='11111', prot_password=None, prot_sig=False, popo=POP_SIG, cr=False): + """Generate a CMP IR or CR request + + :param algorithm_name: str, an algorithm identifier, see ALGORITHMS list + :param server: optional str, URL to use for sending the CMP request + :param recipient: optional str, x509 DN of the recipient of the CMP request, formatted in accordance with the + OpenSSL syntax, see `-subject name` in manpages + :param reference: optional str, request reference number to put in the payload + :param prot_password: optional str, if set, password protection will be used for this request + :param prot_sig: optional bool, if set, will use signature-based protection, overriding the password-based one above + :param popo: optional enum, proof of possession, see POP_SIG definition above + :param cr: optional bool, if set, will create a CR request instead of IR + :return: nothing, but will write the resulting CMP request payload to a file if all is well + """ + features = 'prot_sig' if prot_sig else 'prot_pass' if prot_password else 'prot_none' features += f'-pop_{POP_INVERTED[popo]}' # since these request types are virtually identical, we only need to change this string in one place @@ -136,29 +153,40 @@ def command_generate_cmp_ir(algorithm_name, server='127.17.0.2:8000/pkix', recip extended_path = f'{OUTPUT_PATH}{extended_algorithm_name}/' resulting_file = f'{extended_path}req-{ir_or_cr}-{features}.pkimessage' - protection = f'-secret pass:{password}' if password else '-unprotected_requests' + + protection = f'-secret pass:{prot_password}' if prot_password else '-unprotected_requests' + # override the above if we use signature protection (so it is the same as the logic in `features` + if prot_sig: + protection = f'-cert {DIR_PROTECTION_DATA}ee.crt -key {DIR_PROTECTION_DATA}ee-priv_key.pem' + + # NOTE: you can also specify the subject directly via a command line, this way there is no need to generate a CSR + # beforehand, e.g., `-subject "/CN=xxxxxxxxxxEnd Entity demo"`. However, the code is more reusable with CSR, + # since we can use it for P10CR with fewer changes command = f'openssl cmp -cmd {ir_or_cr} -server {server} -recipient "{recipient}" -ref {reference} ' \ f'-csr {extended_path}csr.pem ' \ - f'-certout {extended_path}cl_cert.pem -newkey {extended_path}key.pem ' \ + f'-certout {extended_path}cl_cert.pem -newkey {extended_path}priv_key.pem ' \ f'-popo {popo} {protection} ' \ f'-reqout {resulting_file}' return run_command(command) -def command_generate_cmp_cr(algorithm_name, server='127.17.0.2:8000/pkix', recipient='/CN=PQCA', reference='11111', password=None, popo=POP_SIG): - return command_generate_cmp_ir(algorithm_name, server, recipient, reference, password, popo, cr=True) +def command_generate_cmp_cr(algorithm_name, server='127.17.0.2:8000/pkix', recipient='/CN=PQCA', reference='11111', prot_password=None, prot_sig=False, popo=POP_SIG): + return command_generate_cmp_ir(algorithm_name, server, recipient, reference, prot_password, prot_sig, popo, cr=True) -def command_generate_cmp_p10cr(algorithm_name, server='127.17.0.2:8000/pkix', reference='11111', password=None, popo=POP_SIG): - features = 'prot_pass' if password else 'prot_none' +def command_generate_cmp_p10cr(algorithm_name, server='127.17.0.2:8000/pkix', reference='11111', prot_password=None, prot_sig=False, popo=POP_SIG): + features = 'prot_sig' if prot_sig else 'prot_pass' if prot_password else 'prot_none' features += f'-pop_{POP_INVERTED[popo]}' extended_algorithm_name = f'{ALG_OID[algorithm_name]}-{algorithm_name}' extended_path = f'{OUTPUT_PATH}{extended_algorithm_name}/' resulting_file = f'{extended_path}req-p10cr-{features}.pkimessage' - protection = f'-secret pass:{password}' if password else '-unprotected_requests' + protection = f'-secret pass:{prot_password}' if prot_password else '-unprotected_requests' + # override the above if we use signature protection (so it is the same as the logic in `features` + if prot_sig: + protection = f'-cert {DIR_PROTECTION_DATA}ee.crt -key {DIR_PROTECTION_DATA}ee-priv_key.pem' command = f'openssl cmp -cmd p10cr -server {server} -ref {reference} ' \ f'-csr {extended_path}csr.pem ' \ @@ -231,10 +259,14 @@ def command_generate_cmp_p10cr(algorithm_name, server='127.17.0.2:8000/pkix', re for function in functions: LOG.info('--------------- Generating %s', function.__name__) - for password, popo in product(passwords, popos): - LOG.info(f'Protection: {f"password {password}" if password else "none"}, Proof-of-possession: {POP_INVERTED[popo]}') - output, status = function(algorithm, password=password, popo=popo) - if status: - LOG.error('Failed, status %s %s', status, output) + output, status = function(algorithm, prot_password='11111') + if status: + LOG.error('Failed, status %s %s', status, output) + output, status = function(algorithm, prot_sig=True) + if status: + LOG.error('Failed, status %s %s', status, output) + output, status = function(algorithm) # no protection + if status: + LOG.error('Failed, status %s %s', status, output) LOG.info('Done') diff --git a/cmp/oqs-openssl/qsckeys.env b/cmp/oqs-openssl/qsckeys.env deleted file mode 100644 index d5b93b44..00000000 --- a/cmp/oqs-openssl/qsckeys.env +++ /dev/null @@ -1,26 +0,0 @@ -OQS_ENCODING_DILITHIUM2=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_DILITHIUM3=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_DILITHIUM5=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_DILITHIUM2_AES=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_DILITHIUM3_AES=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_DILITHIUM5_AES=draft-uni-qsckeys-dilithium-00/sk-pk -OQS_ENCODING_FALCON512=draft-uni-qsckeys-falcon-00/sk-pk -OQS_ENCODING_FALCON1024=draft-uni-qsckeys-falcon-00/sk-pk -OQS_ENCODING_SPHINCSHARAKA128FROBUST=draft-uni-qsckeys-sphincsplus-00/sk-pk -OQS_ENCODING_SPHINCSHARAKA128FSIMPLE=draft-uni-qsckeys-sphincsplus-00/sk-pk -OQS_ENCODING_SPHINCSSHA256128FROBUST=draft-uni-qsckeys-sphincsplus-00/sk-pk -OQS_ENCODING_SPHINCSSHA256128SSIMPLE=draft-uni-qsckeys-sphincsplus-00/sk-pk -OQS_ENCODING_SPHINCSSHAKE256128FSIMPLE=draft-uni-qsckeys-sphincsplus-00/sk-pk -OQS_ENCODING_DILITHIUM2_ALGNAME=Dilithium2 -OQS_ENCODING_DILITHIUM3_ALGNAME=Dilithium3 -OQS_ENCODING_DILITHIUM5_ALGNAME=Dilithium5 -OQS_ENCODING_DILITHIUM2_AES_ALGNAME=Dilithium2_AES -OQS_ENCODING_DILITHIUM3_AES_ALGNAME=Dilithium3_AES -OQS_ENCODING_DILITHIUM5_AES_ALGNAME=Dilithium5_AES -OQS_ENCODING_FALCON512_ALGNAME=Falcon512 -OQS_ENCODING_FALCON1024_ALGNAME=Falcon1024 -OQS_ENCODING_SPHINCSHARAKA128FROBUST_ALGNAME=sphincs-haraka-128f-robust -OQS_ENCODING_SPHINCSHARAKA128FSIMPLE_ALGNAME=sphincs-haraka-128f-simple -OQS_ENCODING_SPHINCSSHA256128FROBUST_ALGNAME=sphincs-sha256-128f-robust -OQS_ENCODING_SPHINCSSHA256128SSIMPLE_ALGNAME=sphincs-sha256-128s-simple -OQS_ENCODING_SPHINCSSHAKE256128FSIMPLE_ALGNAME=sphincs-shake256-128f-simple \ No newline at end of file diff --git a/cmp/oqs-openssl/readme.md b/cmp/oqs-openssl/readme.md index 7809d9f4..dd390ed8 100644 --- a/cmp/oqs-openssl/readme.md +++ b/cmp/oqs-openssl/readme.md @@ -9,10 +9,6 @@ This directory contains examples of CMP payloads generated with OpenSSL and the If you use OpenSSL with OQS via docker, you can rely on the `--docker` parameter, e.g.: `python3 gen.py --docker="docker run -it --rm --volume /home/debdeveu/code/pq-crypto-experiment/dockerdata:/data openquantumsafe/oqs-ossl3" /data/`. It will mount the path `/home/debdeveu/code/pq-crypto-experiment/dockerdata` to `/data` inside the container, and store the output in `/data` (relative to the container). -## Using QSCKeys -A family of RFC drafts describes a way to encode keys. This alternative encoding can be enabled by setting specific environment variables in your system, or passing them to `docker`. The variables are stored in `qsckeys.env`, and an example docker command for it would be: `python3 gen.py --docker="docker run -it --rm --env-file qsckeys.env --volume /home/debdeveu/code/pq-crypto-experiment/dockerdata-qsc:/data-qsc openquantumsafe/oqs-ossl3" /data-qsc/`. - -For details about this encoding, see https://github.com/open-quantum-safe/oqs-provider/blob/main/ALGORITHMS.md#key-encodings # What it does Essentially, it concatenates strings to build a command line for `openssl` and stores the outputs in files. Here is an example of such commands from the log, in case you want to run them by hand: diff --git a/cmp/oqs-openssl/req-cr-dilithium2-pop_none.bin b/cmp/oqs-openssl/req-cr-dilithium2-pop_none.bin deleted file mode 100644 index 30dbac15..00000000 Binary files a/cmp/oqs-openssl/req-cr-dilithium2-pop_none.bin and /dev/null differ diff --git a/cmp/oqs-openssl/req-ir-dilithium2-pop_none.bin b/cmp/oqs-openssl/req-ir-dilithium2-pop_none.bin deleted file mode 100644 index e4a758e6..00000000 Binary files a/cmp/oqs-openssl/req-ir-dilithium2-pop_none.bin and /dev/null differ diff --git a/cmp/oqs-openssl/req-p10cr-dilithium2-pop_none.bin b/cmp/oqs-openssl/req-p10cr-dilithium2-pop_none.bin deleted file mode 100644 index cb99f94d..00000000 Binary files a/cmp/oqs-openssl/req-p10cr-dilithium2-pop_none.bin and /dev/null differ diff --git a/cmp/prerequisites/protection-data/ca-csr.pem b/cmp/prerequisites/protection-data/ca-csr.pem new file mode 100644 index 00000000..07dce363 --- /dev/null +++ b/cmp/prerequisites/protection-data/ca-csr.pem @@ -0,0 +1,7 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIHOMHcCAQAwGDEWMBQGA1UEAwwNSXNzdWluZyBDQXNpZzBWMBAGByqGSM49AgEG +BSuBBAAKA0IABGR/MdEC1kiB0ICWz51NylwzLD70dQzRobCz0FUUelWBrJYKOm/3 +9osEyzJYR8UXxI2LSiyOPfmqozkiKBgNEYKgADAKBggqhkjOPQQDAgNHADBEAiBV +xXrZFtgQbX5+3RRhY0J/yExsko/+XTgxWW/nPjMLiAIgVyJ5sSCdZRciPECG2jgd +TmwSt5Qhw7zHQ4PT3E+kNWo= +-----END CERTIFICATE REQUEST----- diff --git a/cmp/prerequisites/protection-data/ca-priv_key.pem b/cmp/prerequisites/protection-data/ca-priv_key.pem new file mode 100644 index 00000000..c4d37806 --- /dev/null +++ b/cmp/prerequisites/protection-data/ca-priv_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIKUN3WOKOBLdYT6O4prD7GsiDxodQ6zhk43mhPSpOhSJoAcGBSuBBAAK +oUQDQgAEZH8x0QLWSIHQgJbPnU3KXDMsPvR1DNGhsLPQVRR6VYGslgo6b/f2iwTL +MlhHxRfEjYtKLI49+aqjOSIoGA0Rgg== +-----END EC PRIVATE KEY----- diff --git a/cmp/prerequisites/protection-data/ca.crt b/cmp/prerequisites/protection-data/ca.crt new file mode 100644 index 00000000..b1babb69 --- /dev/null +++ b/cmp/prerequisites/protection-data/ca.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBJzCBzgIUYBDCwbCLeDVWILnGovYXmCLua1QwCgYIKoZIzj0EAwIwGDEWMBQG +A1UEAwwNSXNzdWluZyBDQXNpZzAeFw0yMzExMDUxMDEyMTJaFw0zMzExMDIxMDEy +MTJaMBgxFjAUBgNVBAMMDUlzc3VpbmcgQ0FzaWcwVjAQBgcqhkjOPQIBBgUrgQQA +CgNCAARkfzHRAtZIgdCAls+dTcpcMyw+9HUM0aGws9BVFHpVgayWCjpv9/aLBMsy +WEfFF8SNi0osjj35qqM5IigYDRGCMAoGCCqGSM49BAMCA0gAMEUCIBaSSTt4kAIP +t4FadhoEr6FKoNtnfpiiiMhp8kXqnOpFAiEAhdfRoOT3XOvlnCQ+MVUmPcZjt03m +LrB9j8x2o/nhBjQ= +-----END CERTIFICATE----- diff --git a/cmp/prerequisites/protection-data/ee-csr.pem b/cmp/prerequisites/protection-data/ee-csr.pem new file mode 100644 index 00000000..c66fd81c --- /dev/null +++ b/cmp/prerequisites/protection-data/ee-csr.pem @@ -0,0 +1,7 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIHQMHkCAQAwGjEYMBYGA1UEAwwPRW5kIEVudGl0eSBkZW1vMFYwEAYHKoZIzj0C +AQYFK4EEAAoDQgAEmJTzEz0z4m6BmU4tOTJ3vjCrMEFnZWZInkgDBqDB2stZXyBb +j9b+k4QYgxynzR2reDaLZSHLJkdxsfQsy37YNqAAMAoGCCqGSM49BAMCA0cAMEQC +IFfPv83ci87Xqc6tldLxZo3dgBZVw5dZXqlcqMbW5ZfgAiBviu7Ad72mdT1EILvm +CHMOcaeZphc4zXe26TQoOvtQZg== +-----END CERTIFICATE REQUEST----- diff --git a/cmp/prerequisites/protection-data/ee-priv_key.pem b/cmp/prerequisites/protection-data/ee-priv_key.pem new file mode 100644 index 00000000..bb2192ef --- /dev/null +++ b/cmp/prerequisites/protection-data/ee-priv_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEINudqscEZcHy5K19a6DOLxQ49CYNUa3nT623iCxWssTroAcGBSuBBAAK +oUQDQgAEmJTzEz0z4m6BmU4tOTJ3vjCrMEFnZWZInkgDBqDB2stZXyBbj9b+k4QY +gxynzR2reDaLZSHLJkdxsfQsy37YNg== +-----END EC PRIVATE KEY----- diff --git a/cmp/prerequisites/protection-data/ee.crt b/cmp/prerequisites/protection-data/ee.crt new file mode 100644 index 00000000..a9383db8 --- /dev/null +++ b/cmp/prerequisites/protection-data/ee.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBKjCB0gIUUQ1HPnXf00Ze7EZo459MvJfs6IMwCgYIKoZIzj0EAwIwGjEYMBYG +A1UEAwwPRW5kIEVudGl0eSBkZW1vMB4XDTIzMTEwNTEwMTIxMloXDTI4MTEyODEw +MTIxMlowGjEYMBYGA1UEAwwPRW5kIEVudGl0eSBkZW1vMFYwEAYHKoZIzj0CAQYF +K4EEAAoDQgAEmJTzEz0z4m6BmU4tOTJ3vjCrMEFnZWZInkgDBqDB2stZXyBbj9b+ +k4QYgxynzR2reDaLZSHLJkdxsfQsy37YNjAKBggqhkjOPQQDAgNHADBEAiAI3Z2j +beomWnajSljAbz3COlxspVe0Kku/znMLvh0pHQIgDBCO1V7qYxmP49bgtzdOOMtQ +uHp6x06Hbv7hU9z3dXM= +-----END CERTIFICATE----- diff --git a/cmp/prerequisites/protection-data/generate.sh b/cmp/prerequisites/protection-data/generate.sh new file mode 100644 index 00000000..a42aa724 --- /dev/null +++ b/cmp/prerequisites/protection-data/generate.sh @@ -0,0 +1,11 @@ +# You can also run it as `bash readme.md` +# For the CA +openssl ecparam -name secp256k1 -genkey -noout -out ca-priv_key.pem +openssl req -out ca-csr.pem -new -key ca-priv_key.pem -nodes -subj "/CN=Issuing CAsig" +openssl x509 -signkey ca-priv_key.pem -in ca-csr.pem -req -days 3650 -out ca.crt + + +# For the EE +openssl ecparam -name secp256k1 -genkey -noout -out ee-priv_key.pem +openssl req -out ee-csr.pem -new -key ee-priv_key.pem -nodes -subj "/CN=End Entity demo" +openssl x509 -signkey ee-priv_key.pem -in ee-csr.pem -req -days 1850 -out ee.crt diff --git a/cmp/readme.md b/cmp/readme.md index 8fa1ddd1..3efe8fdf 100644 --- a/cmp/readme.md +++ b/cmp/readme.md @@ -12,15 +12,34 @@ Each subdirectory contains ## Artifacts -Artifacts are stored in directories that follow the `oid-algorithm` pattern, e.g., `1.3.6.1.4.1.2.267.7.4.4-dilithium2`. -Each directory contains files like `req-p10cr-prot_pass-pop_sig.pkimessage`. The name contains multiple components separated by `-`, and each component can contain `_` to indicate additional information related to it. The structure is as follows: +Artifacts are stored in directories that follow the `oid-` pattern, e.g., `1.3.6.1.4.1.2.267.7.4.4-dilithium2`. +The human-readable part is optional, it is there to assist those who do not yet remember the OIDs by heart. + +Each directory contains files like `req-p10cr-prot_pass-pop_sig.pkimessage`. The name contains multiple components +separated by `-`, and each component can contain `_` to indicate additional information related to it. +The structure is as follows: - `req` or `rep`: indicates whether it is a request or a response - message type, e.g. `cr`, `ir`, `p10cr` and so on - `prot_` indicates the type of protection used, e.g., "none" or "password-based" - `pop_` stands for "proof of possession", e.g. "signature-based" -Other files available for each algorithm are `key.pem`, `csr.pem` for the key and certificate-signing request respectively. +Other files available for each algorithm are: + +- `priv_key.pem` - the private key. +- Optionally, `pub_key.pem` (e.g., for algorithms where it cannot be derived from the private key). +- `csr.pem` - certificate-signing request. + + +### Other prerequisites +Some of the CMP features require other files (e.g., keys), they are stored in `prerequisites/`. + +#### Signature-based protection +These files are needed for testing signature-based protection of CMP messages, following [this use-case of RFC 4210](https://datatracker.ietf.org/doc/html/rfc4210#page-79). +Examine `prerequisites/protection-data/generate.sh` to see how these files are generated. + + + # Automation If payloads can be generated automatically, add a script called `gen` (add file extension as necessary). diff --git a/cmp/validator/readme.md b/cmp/validator/readme.md new file mode 100644 index 00000000..c80ab4be --- /dev/null +++ b/cmp/validator/readme.md @@ -0,0 +1,20 @@ +# Overview + +Use the RFC9480 parser from pyasn1-alt-modules to parse all the payloads collected in this repository and see if they're valid `PKIMessage` structures. + +# Usage +`python validate.py work/pqc-certificates/cmp/oqs-openssl/artifacts`. + + +## How it works +- Go through all the directories and look for files that match the `*.pkimessage` name. +- Attempt to parse them as `RFC9480.PKIMessage`. +- If it fails, show the path to the problematic payload and the error message. + +# Installation +Create a Python virtualenv and run: + +- `pip install pyasn1` +- `pip install -e git+https://github.com/russhousley/pyasn1-alt-modules.git@master#egg=pyasn1-alt-modules` + +Note that it uses the newest definition of PKIMessage from RFC9480, currently available only in the Github repository. Eventually it will become part of a `pyasn1-alt-modules` release, so you'd be able to install that right away. \ No newline at end of file diff --git a/cmp/validator/validate.py b/cmp/validator/validate.py new file mode 100644 index 00000000..b3e696f8 --- /dev/null +++ b/cmp/validator/validate.py @@ -0,0 +1,44 @@ +import os +import argparse +from pyasn1.codec.der import decoder +from pyasn1_alt_modules import rfc9480 + + +def parse_payload(raw): + """Attempt to parse the raw buffer as a PKIMessage""" + parsed_data, _ = decoder.decode(raw, asn1Spec=rfc9480.PKIMessage()) + return parsed_data + + + +def process_pki_messages(directory): + files_parsed = 0 + errors = 0 + for root, _, files in os.walk(directory): + for file in files: + if file.endswith('.pkimessage'): + file_path = os.path.join(root, file) + try: + with open(file_path, 'rb') as f: + raw_data = f.read() + parsed_data = parse_payload(raw_data) + print(f"OK '{file}'") + except Exception as e: + errors += 1 + print(f"ERR '{file_path}': {str(e)[:60]}...") + else: + files_parsed += 1 + + print(f'OK: {files_parsed}\tERR: {errors}') + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Parse PKIMessage files in directories') + parser.add_argument('directory', help='Path to the directory to start from') + args = parser.parse_args() + + if os.path.isdir(args.directory): + process_pki_messages(args.directory) + else: + print(f"Error: The provided path '{args.directory}' is not a directory.")