Skip to content

Keys and encodings

A composite key pair is one ML-DSA key pair and one traditional key pair that are only ever used together. The algorithm fixes both components, for example MLDSA65ECDSAP256SHA512 pairs ML-DSA-65 with ECDSA on P-256.

Generating keys

key, err := compositemldsa.GenerateKey(compositemldsa.MLDSA65ECDSAP256SHA512)

GenerateKey creates both components fresh. The draft forbids reusing a component key in another composite key or on its own, because a key used in both settings weakens the guarantee that the two signatures cannot be separated. Do not build a composite key from an existing RSA or ECDSA key.

*PrivateKey implements crypto.Signer and crypto.MessageSigner. key.PublicKey() returns the *PublicKey. key.Public() returns the same key as crypto.PublicKey.

Encodings

Form Public key Private key
Raw bytes pk.Bytes() and NewPublicKey sk.Bytes() and NewPrivateKey
DER MarshalPKIXPublicKey and ParsePKIXPublicKey, a SubjectPublicKeyInfo MarshalPKCS8PrivateKey and ParsePKCS8PrivateKey, PKCS #8
PEM DER in a PUBLIC KEY block DER in a PRIVATE KEY block

The raw public key is the ML-DSA public key followed by the traditional one: an RSA RSAPublicKey, an uncompressed ECDSA point or 32 Ed25519 bytes. The raw private key is the 32-byte ML-DSA seed followed by an RSA RSAPrivateKey, an ECDSA ECPrivateKey or the 32-byte Ed25519 seed. The DER forms wrap the raw bytes under the algorithm's OID with parameters absent. These are the encodings of section 4 and section 5 of the draft. The library reproduces the draft's test vectors byte for byte.

Store a private key as PKCS #8:

der, err := compositemldsa.MarshalPKCS8PrivateKey(key)
if err != nil {
    return err
}
err = os.WriteFile("key.pem", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600)

MarshalPKCS8PrivateKey writes a version 0 PrivateKeyInfo. ParsePKCS8PrivateKey also accepts a version 1 OneAsymmetricKey and checks that an included public key matches the private key. Attributes must be well formed and are discarded.

Keys of any type

compositex509.ParsePKIXPublicKey, MarshalPKIXPublicKey, ParsePKCS8PrivateKey and MarshalPKCS8PrivateKey work like their crypto/x509 counterparts and also handle composite keys. Use them where a program reads keys of several types:

key, err := compositex509.ParsePKCS8PrivateKey(der)
if err != nil {
    return err
}
switch k := key.(type) {
case *compositemldsa.PrivateKey:
    // composite
case *ecdsa.PrivateKey:
    // classical
default:
    _ = k
}

Strict parsing

Parsing rejects anything the draft does not allow:

  • algorithm identifiers with parameters
  • keys of the wrong length for the algorithm
  • RSA keys whose modulus is not exactly the size the algorithm names
  • ECDSA points that are not on the algorithm's curve
  • traditional keys in any encoding other than the single DER form section 4 of the draft fixes, such as an ECDSA private key that carries its public key
  • ML-DSA private keys in the expanded form, since the draft stores only the seed
  • PKCS #8 keys with malformed attributes, a malformed public key or elements after the public key
  • trailing data after any structure

Components

pk.MLDSAPublicKey() and pk.TraditionalPublicKey() expose the components for inspection, for example to check a component against a revocation list. The draft recommends that a CA checking a new composite key for earlier compromise also checks each component key. Never verify with a component on its own.