lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <1176796.1768921455@warthog.procyon.org.uk>
Date: Tue, 20 Jan 2026 15:04:15 +0000
From: David Howells <dhowells@...hat.com>
To: Eric Biggers <ebiggers@...nel.org>,
    Stephan Mueller <smueller@...onox.de>
Cc: dhowells@...hat.com, linux-crypto@...r.kernel.org,
    linux-kernel@...r.kernel.org, Ard Biesheuvel <ardb@...nel.org>,
    "Jason A . Donenfeld" <Jason@...c4.com>,
    Herbert Xu <herbert@...dor.apana.org.au>
Subject: Python script to generate X509/CMS from NIST testcases

Hi Eric, Stephan,

In case it turns out to be useful to you as a template, here's a script that I
wrote to package NIST ML-DSA testcases from JSON files into rudimentary X.509,
message and CMS signature files and also to produce a C file that contains
those blobs packaged into u8 arrays with a table listing them all.

It also tries to verify each testcase with "openssl smime" - except that that
doesn't work too will for ML-DSA (it did work for RSASSA-PSS, but that's
another script).

David
---
#!/usr/bin/python3
#
# Generate X.509 certificates and CMS messages from NIST ML-DSA SigVer test
# vectors (e.g. prompt.json).

import os
import sys
import datetime
import subprocess
import asn1tools
import json

if len(sys.argv) < 3:
    print("Format x509_gen.py <prompt.json> <expectedResults.json>", file=sys.stderr)
    exit(2)

OID_rsaEncryption               = "1.2.840.113549.1.1.1"
OID_sha1WithRSAEncryption       = "1.2.840.113549.1.1.5"
OID_id_mgf1                     = "1.2.840.113549.1.1.8"
OID_id_rsassa_pss               = "1.2.840.113549.1.1.10"
OID_sha256WithRSAEncryption     = "1.2.840.113549.1.1.11"
OID_sha384WithRSAEncryption     = "1.2.840.113549.1.1.12"
OID_sha512WithRSAEncryption     = "1.2.840.113549.1.1.13"
OID_sha224WithRSAEncryption     = "1.2.840.113549.1.1.14"
OID_sha1                        = "1.3.14.3.2.26"
OID_sha256                      = "2.16.840.1.101.3.4.2.1"
OID_sha384                      = "2.16.840.1.101.3.4.2.2"
OID_sha512                      = "2.16.840.1.101.3.4.2.3"
OID_sha224                      = "2.16.840.1.101.3.4.2.4"
OID_commonName                  = "2.5.4.3"
OID_subjectKeyIdentifier        = "2.5.29.14"
OID_keyUsage                    = "2.5.29.15"
OID_basicConstraints            = "2.5.29.19"
OID_data                        = "1.2.840.113549.1.7.1"
OID_signed_data                 = "1.2.840.113549.1.7.2"
OID_id_ml_dsa_44                = "2.16.840.1.101.3.4.3.17"
OID_id_ml_dsa_65                = "2.16.840.1.101.3.4.3.18"
OID_id_ml_dsa_87                = "2.16.840.1.101.3.4.3.19"

###############################################################################
#
# ASN.1 definitions
#
###############################################################################
RSA = asn1tools.compile_string("""
Rsa DEFINITIONS ::= BEGIN
RsaPubKey ::= SEQUENCE {
	n INTEGER,
	e INTEGER
}
RSASSA-PSS-params ::= SEQUENCE {
	hashAlgorithm      [0] HashAlgorithm,
	maskGenAlgorithm   [1] MaskGenAlgorithm,
	saltLength         [2] INTEGER,
	trailerField       [3] TrailerField OPTIONAL
}
HashAlgorithm ::= AlgorithmIdentifier
MaskGenAlgorithm ::= AlgorithmIdentifier
TrailerField ::= INTEGER
AlgorithmIdentifier ::= SEQUENCE {
	algorithm	OBJECT IDENTIFIER,
	parameters	ANY OPTIONAL
}
END""", 'der')

#------------------------------------------------------------------------------
# X.509
#
X509 = asn1tools.compile_string("""
X509 DEFINITIONS ::= BEGIN
Certificate ::= SEQUENCE {
	tbsCertificate		TBSCertificate,
	signatureAlgorithm	AlgorithmIdentifier,
	signature		BIT STRING
	}
TBSCertificate ::= SEQUENCE {
	version           [ 0 ]	Version,
	serialNumber		CertificateSerialNumber,
	signature		AlgorithmIdentifier,
	issuer			Name,
	validity		Validity,
	subject			Name,
	subjectPublicKeyInfo	SubjectPublicKeyInfo,
	issuerUniqueID    [ 1 ]	IMPLICIT UniqueIdentifier OPTIONAL,
	subjectUniqueID   [ 2 ]	IMPLICIT UniqueIdentifier OPTIONAL,
	extensions        [ 3 ]	Extensions OPTIONAL
	}
Version ::= INTEGER
CertificateSerialNumber ::= INTEGER
AlgorithmIdentifier ::= SEQUENCE {
	algorithm		OBJECT IDENTIFIER,
	parameters		ANY OPTIONAL
}
Name ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET OF AttributeValueAssertion
AttributeValueAssertion ::= SEQUENCE {
	attributeType		OBJECT IDENTIFIER,
	attributeValue		UTF8String -- Really ANY
	}
Validity ::= SEQUENCE {
	notBefore		Time,
	notAfter		Time
	}
Time ::= CHOICE {
	utcTime			UTCTime,
	generalTime		GeneralizedTime
	}
SubjectPublicKeyInfo ::= SEQUENCE {
	algorithm		AlgorithmIdentifier,
	subjectPublicKey	BIT STRING
	}
UniqueIdentifier ::= BIT STRING
Extensions ::= SEQUENCE OF Extension
Extension ::= SEQUENCE {
	extnid			OBJECT IDENTIFIER,
	critical		BOOLEAN,
	extnValue		OCTET STRING
	}
END""", 'der')

#------------------------------------------------------------------------------
# PKCS#7
#
PKCS7 = asn1tools.compile_string("""
PKCS7 DEFINITIONS ::= BEGIN
PKCS7ContentInfo ::= SEQUENCE {
	contentType	ContentType,
	content		[0] EXPLICIT SignedData OPTIONAL
}
ContentType ::= OBJECT IDENTIFIER
SignedData ::= SEQUENCE {
	version			INTEGER,
	digestAlgorithms	DigestAlgorithmIdentifiers,
	contentInfo		ContentInfo,
	certificates		CHOICE {
		certSet		[0] IMPLICIT ExtendedCertificatesAndCertificates,
		certSequence	[2] IMPLICIT Certificates
	} OPTIONAL,
	crls CHOICE {
		crlSet		[1] IMPLICIT CertificateRevocationLists,
		crlSequence	[3] IMPLICIT CRLSequence
	} OPTIONAL,
	signerInfos		SignerInfos
}
ContentInfo ::= SEQUENCE {
	contentType	ContentType,
	content		[0] EXPLICIT Data OPTIONAL
}
Data ::= ANY
DigestAlgorithmIdentifiers ::= CHOICE {
	daSet			SET OF DigestAlgorithmIdentifier,
	daSequence		SEQUENCE OF DigestAlgorithmIdentifier
}
DigestAlgorithmIdentifier ::= SEQUENCE {
	algorithm   OBJECT IDENTIFIER,
	parameters  ANY OPTIONAL
}
ExtendedCertificatesAndCertificates ::= SET OF ExtendedCertificateOrCertificate
ExtendedCertificateOrCertificate ::= CHOICE {
  certificate		Certificate,
  extendedCertificate	[0] IMPLICIT ExtendedCertificate
}
ExtendedCertificate ::= Certificate
Certificates ::= SEQUENCE OF Certificate
CertificateRevocationLists ::= SET OF CertificateList
CertificateList ::= SEQUENCE OF Certificate
CRLSequence ::= SEQUENCE OF CertificateList
Certificate ::= BOOLEAN -- This really needs to be ANY, but asn1tools explodes
SignerInfos ::= CHOICE {
	siSet		SET OF SignerInfo,
	siSequence	SEQUENCE OF SignerInfo
}
SignerInfo ::= SEQUENCE {
	version			INTEGER,
	sid			SignerIdentifier,
	digestAlgorithm		DigestAlgorithmIdentifier,
	authenticatedAttributes	CHOICE {
		aaSet		[0] IMPLICIT SetOfAuthenticatedAttribute,
		aaSequence	[2] EXPLICIT SEQUENCE OF AuthenticatedAttribute
	} OPTIONAL,
	digestEncryptionAlgorithm
				DigestEncryptionAlgorithmIdentifier,
	encryptedDigest		EncryptedDigest,
	unauthenticatedAttributes CHOICE {
		uaSet		[1] IMPLICIT SET OF UnauthenticatedAttribute,
		uaSequence	[3] IMPLICIT SEQUENCE OF UnauthenticatedAttribute
	} OPTIONAL
}
SignerIdentifier ::= CHOICE {
	issuerAndSerialNumber IssuerAndSerialNumber,
        subjectKeyIdentifier [0] IMPLICIT SubjectKeyIdentifier
}
IssuerAndSerialNumber ::= SEQUENCE {
	issuer			Name,
	serialNumber		CertificateSerialNumber
}
CertificateSerialNumber ::= INTEGER
SubjectKeyIdentifier ::= OCTET STRING
SetOfAuthenticatedAttribute ::= SET OF AuthenticatedAttribute
AuthenticatedAttribute ::= SEQUENCE {
	type			OBJECT IDENTIFIER,
	values			SET OF ANY
}
UnauthenticatedAttribute ::= SEQUENCE {
	type			OBJECT IDENTIFIER,
	values			SET OF ANY
}
DigestEncryptionAlgorithmIdentifier ::= SEQUENCE {
	algorithm		OBJECT IDENTIFIER,
	parameters		ANY OPTIONAL
}
EncryptedDigest ::= OCTET STRING
Name ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET OF AttributeValueAssertion
AttributeValueAssertion ::= SEQUENCE {
	attributeType		OBJECT IDENTIFIER,
	attributeValue		UTF8String -- Really ANY
}
END""", 'der')

###############################################################################
#
# Write a C data array from a bytestring.
#
###############################################################################
def write_c_hexarray(cfile, name, data):
    cfile.write("static const u8 " + name + "[] __initconst = {\n")
    need_close = False
    for i in range(0, len(data)):
        if not need_close:
            cfile.write("\t\"")
            need_close = True
        cfile.write("\\x{:02x}".format(data[i]))
        if i & 0xf == 0xf:
            cfile.write("\"\n")
            need_close = False
    if need_close:
            cfile.write("\"\n")
    cfile.write("};\n")

vector_table = list()

def write_c(cfile, name, x509, data, pkcs7, result):
    write_c_hexarray(cfile, name + "_key", x509)
    write_c_hexarray(cfile, name + "_data", data)
    write_c_hexarray(cfile, name + "_sig", pkcs7)
    cfile.write("\n")

    vector_table.append("\t{\n");
    vector_table.append("\t\t.name\t\t= \"" + name + "\",\n");
    vector_table.append("\t\t.key\t\t= "  + name + "_key,\n");
    vector_table.append("\t\t.data\t\t= " + name + "_data,\n");
    vector_table.append("\t\t.sig\t\t= "  + name + "_sig,\n");
    vector_table.append("\t\t.key_len\t= sizeof("  + name + "_key) - 1,\n");
    vector_table.append("\t\t.data_len\t= sizeof("  + name + "_data) - 1,\n");
    vector_table.append("\t\t.sig_len\t= sizeof("  + name + "_sig) - 1,\n");
    vector_table.append("\t\t.pass\t\t= " + result + ",\n");
    vector_table.append("\t},\n");

def write_c_table(cfile, basename):
    cfile.write("const struct nist_test_vector " + basename + "[] __initconst = {\n")
    for i in vector_table:
        cfile.write(i)
    cfile.write("};\n")


###############################################################################
#
# Create an X.509 certificate to hold an RSA public key and create a detached
# PKCS#7 message to carry a signature created with it.
#
###############################################################################
def create_rsa_key(n, e):
    """Create an RSA public key"""
    pubkey = RSA.encode("RsaPubKey", {
        'n' : rsa_n,
        'e' : rsa_e
        })

def create_rsassa_params(salt_len, digest_alg):
    """Create the parameters for RSASSA-PSS"""
    mgf1_params = RSA.encode("AlgorithmIdentifier", {
        'algorithm'         : digest_alg,
        'parameters'        : None
    })

    sig_params = RSA.encode("RSASSA-PSS-params", {
            'hashAlgorithm'         : {
                'algorithm'         : digest_alg,
                'parameters'        : None
            },
            'maskGenAlgorithm'      : {
                'algorithm'         : OID_id_mgf1,
                'parameters'        : mgf1_params
                },
            'saltLength'            : salt_len,
        })

def create_cert_and_sig(basename, count, pubkey, signature, sig_params, content,
                        digest_alg, sig_alg, result, cfile):
    serial = 0x1234000 + count

    # Create an X.509 certificate
    x509 = X509.encode("Certificate", {
        'tbsCertificate' : {
            'version'           : 2,
            'serialNumber'      : serial,
            'signature' : {
                'algorithm'         : sig_alg,
                'parameters'        : sig_params
            },
            'issuer' : [
                [
                    {
                        'attributeType' : OID_commonName,
                        'attributeValue' : "Fred"
                    }
                ]
            ],
            'validity' : {
                'notBefore' : ('utcTime',     datetime.datetime(2026, 1, 1)),
                'notAfter'  : ('generalTime', datetime.datetime(2199, 1, 1)),
            },
            'subject' : [
                [
                    {
                        'attributeType' : OID_commonName,
                        'attributeValue' : basename
                    }
                ]
            ],
            'subjectPublicKeyInfo' : {
                'algorithm' : {
                    'algorithm'         : sig_alg,
                    'parameters'        : None
                },
                'subjectPublicKey' : (pubkey, len(pubkey)*8)
            },
            'extensions' : [
                {
                    'extnid'	: OID_basicConstraints,
                    'critical'	: True,
                    'extnValue'	: b'\x30\x00'
                }, {
                    'extnid'	: OID_keyUsage,
                    'critical'	: False,
                    'extnValue'	: b'\x03\x02\x07\x80'
                }, {
                    'extnid'	: OID_subjectKeyIdentifier,
                    'critical'	: False,
                    'extnValue'	: bytes.fromhex("04142B73932CF06C341AA72CCEA4E0AC35A96CCC{:04x}".format(count)),
                },
            ]
        },
        'signatureAlgorithm' :  {
            'algorithm'         : sig_alg,
            'parameters'        : sig_params
        },
        'signature' : (signature, len(signature)*8)
    })

    # Create a detached PKCS#7 message to use as a signature carrier
    pkcs7 = PKCS7.encode("PKCS7ContentInfo", {
        'contentType'               : OID_signed_data,
        'content' : {
            'version'		: 1,
            'digestAlgorithms' : ( 'daSet', [
                {
                    'algorithm'         : digest_alg,
                    'parameters'        : None
                }
            ]),
            'contentInfo' : {
                'contentType'               : OID_data
            },
            'signerInfos' : ('siSet', [
                {
                    'version'		: 1,
                    'sid' : (
                        'issuerAndSerialNumber', {
                            'issuer'		: [
                                [
                                    {
                                        'attributeType' : OID_commonName,
                                        'attributeValue' : "Fred"
                                    }
                                ]
                            ],
                            'serialNumber' : serial
                        }
                    ),
                    'digestAlgorithm' : {
                        'algorithm'         : digest_alg,
                    },
                    'digestEncryptionAlgorithm' : {
                        'algorithm'         : sig_alg,
                        'parameters'        : sig_params
                    },
                    'encryptedDigest'	: signature,
                }
            ])
        }
    })

    out = open(basename + ".x509", "wb")
    out.write(x509)
    out.close()

    out = open(basename + ".p7s", "wb")
    out.write(pkcs7)
    out.close()

    out = open(basename + ".data", "wb")
    out.write(content)
    out.close()

    write_c(cfile, basename, x509, content, pkcs7, result)

###############################################################################
#
# Parse FIPS JSON vector file
#
###############################################################################

vecfilename = sys.argv[1]
vecf = open(vecfilename, "r")
testdata = json.load(vecf);
vecf.close()

expfilename = sys.argv[2]
expf = open(expfilename, "r")
expected = json.load(expf);
expf.close()

cfile = open("nist_testdata.c", "w")

def badfile(msg):
    print(vecfilename + ": " + msg, file=sys.stderr);
    exit(3)
def skipg(tgid, msg):
    print("skipping tgId=" + str(tgid) + ": " + msg, file=sys.stderr)
def skipc(tcid, msg):
    print("skipping tcId=" + str(tcid) + ": " + msg, file=sys.stderr)

if testdata["mode"] != "sigVer":
    badfile("Only sigVer files supported")

if testdata["algorithm"] != "ML-DSA":
    badfile("Unsupported algo " + testdata["algorithm"])

if expected["algorithm"] != testdata["algorithm"] or \
   expected["revision"] != testdata["revision"] or \
   expected["vsId"] != testdata["vsId"] or \
   expected["mode"] != testdata["mode"]:
    badfile("Doesn't match expected data file")

rname = "nist-"
rname += testdata["algorithm"].replace("-", "").lower() + "-"
rname += testdata["revision"].lower()
rname = rname.replace("-", "_")
count = 0

results = dict()
for tgroup in expected["testGroups"]:
    for test in tgroup["tests"]:
        tcid = test["tcId"]
        results[tcid] = test["testPassed"]

for tgroup in testdata["testGroups"]:
    tgid = tgroup["tgId"]

    if tgroup["testType"] != "AFT":
        skipg(tgid, "Not an Algorithm Functional Test")
        continue

    if tgroup["signatureInterface"] == "external":
        if tgroup["preHash"] == "preHash":
            skipg(tgid, "Pre-hashing required")
            continue
        if tgroup["preHash"] != "pure":
            skipg(tgid, "Not pure")
            continue
    else:
        if tgroup["externalMu"]:
            skipg(tgid, "External-mu required")
            continue
        #json.dump(tgroup, sys.stdout)
        #skipg(tgid, "Internal")
        #continue

    if tgroup["parameterSet"] == "ML-DSA-44":
        sig_algo = OID_id_ml_dsa_44
        hash_algo = None
    elif tgroup["parameterSet"] == "ML-DSA-65":
        sig_algo = OID_id_ml_dsa_65
        hash_algo = None
    elif tgroup["parameterSet"] == "ML-DSA-87":
        sig_algo = OID_id_ml_dsa_87
        hash_algo = None
    else:
        badfile("Unsupported algo " + testdata["algorithm"])

    for test in tgroup["tests"]:
        tcid = test["tcId"]
        pubkey = bytes.fromhex(test["pk"])
        message = bytes.fromhex(test["message"])
        signature = bytes.fromhex(test["signature"])
        sig_params = None

        try:
            if test["hashAlg"] == "SHA1":
                digest_algo = OID_sha1
            elif test["hashAlg"] == "SHA224":
                digest_algo = OID_sha224
            elif test["hashAlg"] == "SHA256":
                digest_algo = OID_sha256
            elif test["hashAlg"] == "SHA384":
                digest_algo = OID_sha384
            elif test["hashAlg"] == "SHA512":
                digest_algo = OID_sha512
            else:
                skipc("Unknown algo:", test["hashAlg"])
                continue
        except KeyError:
            if testdata["algorithm"] != "ML-DSA":
                skipc("No hash algo")
                continue
            digest_algo = OID_sha512

        result = results[tcid]
        if result:
            result_val = "true"
        else:
            result_val = "false"

        name = rname + "_" + str(tcid);
        create_cert_and_sig(name, tcid,
                            pubkey, signature, sig_params, message,
                            digest_algo,
                            sig_algo,
                            result_val,
                            cfile)
        count += 1

        os.environ["LD_LIBRARY_PATH"] = "/data/openssl/build/"
        status = subprocess.run([ "/data/openssl/build/apps/openssl",
                                  "smime", "-verify", "-binary",
                                  "-inform", "DER", "-in", name + ".p7s",
                                  "-content", name + ".data",
                                  "-certfile", name + ".x509",
                                  "-nointern",
                                  "-noverify",
                                  "-out", "/dev/null"
                                 ],
                                capture_output = True)

        if status.returncode != 0:
            print("tcId=" + str(tcid) +
                  ": Unexpected failure", name, status.returncode)
            #sys.stderr.buffer.write(status.stderr)
            #exit(4)
        else:
            print("tcId=" + str(tcid) + ": Success", name)

write_c_table(cfile, rname)

print("Wrote", count, "test vectors");


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ