x509: Add VerifyOptions.UnknownAlgorithmVerifier

This allows callers to verify certificates using algorithms that Go
does not support (yet).

For instance, here we're verifying the ML-KEM-512 example certificate
from the LAMPS WG signed by a ML-DSA-44 public key.

    https://github.com/lamps-wg/dilithium-certificates/blob/main/examples/ML-DSA-44.crt
    https://github.com/lamps-wg/kyber-certificates/blob/main/example/ML-KEM-512.crt

package main

import (
        "crypto/x509"
        "crypto/x509/pkix"
        "encoding/asn1"
        "encoding/pem"
        "errors"
        "fmt"
        "github.com/cloudflare/circl/sign/schemes"
        "os"
)

func loadCert(path string) (*x509.Certificate, error) {
        raw, err := os.ReadFile(path)
        if err != nil {
                return nil, fmt.Errorf("ReadFile(%s): %w", path, err)
        }
        block, _ := pem.Decode(raw)
        if block == nil {
                return nil, fmt.Errorf("pem.Decode(%s) failed", path)
        }
        return x509.ParseCertificate(block.Bytes)
}

func main() {
        dsaCert, err := loadCert("ML-DSA-44.crt")
        if err != nil {
                panic(err)
        }
        kemCert, err := loadCert("ML-KEM-512.crt")
        if err != nil {
                panic(err)
        }

        roots := x509.NewCertPool()
        roots.AddCert(dsaCert)
        _, err = kemCert.Verify(x509.VerifyOptions{
                Roots: roots,
                UnknownAlgorithmVerifier: func(alg pkix.AlgorithmIdentifier,
                        signed, signature, pk []byte) error {
                        if !alg.Algorithm.Equal(asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 17}) {
                                return errors.New("unsupported scheme")
                        }
                        scheme := schemes.ByName("ML-DSA-44")
                        ppk, err := scheme.UnmarshalBinaryPublicKey(pk)
                        if err != nil {
                                return err
                        }
                        if !scheme.Verify(ppk, signed, signature, nil) {
                                return errors.New("invalid signature")
                        }
                        return nil
                },
        })
        if err != nil {
                panic(err)
        }
}
This commit is contained in:
Bas Westerbaan 2025-06-06 13:19:43 +02:00
parent 3432c68467
commit fa2ee19f65
4 changed files with 49 additions and 17 deletions

View File

@ -1038,11 +1038,14 @@ func parseCertificate(der []byte) (*Certificate, error) {
if !spki.ReadASN1BitString(&spk) { if !spki.ReadASN1BitString(&spk) {
return nil, errors.New("x509: malformed subjectPublicKey") return nil, errors.New("x509: malformed subjectPublicKey")
} }
if cert.PublicKeyAlgorithm != UnknownPublicKeyAlgorithm { pki := &publicKeyInfo{
cert.PublicKey, err = parsePublicKey(&publicKeyInfo{ Algorithm: pkAI,
Algorithm: pkAI, PublicKey: spk,
PublicKey: spk, }
}) if cert.PublicKeyAlgorithm == UnknownPublicKeyAlgorithm {
cert.PublicKey = pki
} else {
cert.PublicKey, err = parsePublicKey(pki)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -190,8 +190,8 @@ func verifyChain(c *Certificate, chainCtx *syscall.CertChainContext, opts *Verif
if parent.PublicKeyAlgorithm != ECDSA { if parent.PublicKeyAlgorithm != ECDSA {
continue continue
} }
if err := parent.CheckSignature(chain[i].SignatureAlgorithm, if err := checkSignature(chain[i].SignatureAlgorithm,
chain[i].RawTBSCertificate, chain[i].Signature); err != nil { chain[i].RawTBSCertificate, chain[i].Signature, parent.PublicKey, true, opts); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -218,6 +218,10 @@ type VerifyOptions struct {
// field implies any valid policy is acceptable. // field implies any valid policy is acceptable.
CertificatePolicies []OID CertificatePolicies []OID
// UnknownAlgorithmVerifier specifies a callback to use to verify
// a signature with an unknown AlgorithmIdentifier.
UnknownAlgorithmVerifier func(alg pkix.AlgorithmIdentifier, signed, signature, pk []byte) error
// The following policy fields are unexported, because we do not expect // The following policy fields are unexported, because we do not expect
// users to actually need to use them, but are useful for testing the // users to actually need to use them, but are useful for testing the
// policy validation code. // policy validation code.
@ -975,7 +979,7 @@ func (c *Certificate) buildChains(currentChain []*Certificate, sigChecks *int, o
return return
} }
if err := c.CheckSignatureFrom(candidate.cert); err != nil { if err := c.checkSignatureFrom(candidate.cert, opts); err != nil {
if hintErr == nil { if hintErr == nil {
hintErr = err hintErr = err
hintCert = candidate.cert hintCert = candidate.cert

View File

@ -205,6 +205,17 @@ type publicKeyInfo struct {
PublicKey asn1.BitString PublicKey asn1.BitString
} }
func (pki *publicKeyInfo) Equal(other crypto.PublicKey) bool {
pki2, ok := other.(*publicKeyInfo)
if !ok {
return false
}
return (pki.Algorithm.Algorithm.Equal(pki2.Algorithm.Algorithm) &&
bytes.Equal(pki.Algorithm.Parameters.FullBytes, pki2.Algorithm.Parameters.FullBytes) &&
pki.PublicKey.BitLength == pki2.PublicKey.BitLength &&
bytes.Equal(pki.PublicKey.Bytes, pki2.PublicKey.Bytes))
}
// RFC 5280, 4.2.1.1 // RFC 5280, 4.2.1.1
type authKeyId struct { type authKeyId struct {
Id []byte `asn1:"optional,tag:0"` Id []byte `asn1:"optional,tag:0"`
@ -909,6 +920,10 @@ func (c *Certificate) hasSANExtension() bool {
// This is a low-level API that performs very limited checks, and not a full // This is a low-level API that performs very limited checks, and not a full
// path verifier. Most users should use [Certificate.Verify] instead. // path verifier. Most users should use [Certificate.Verify] instead.
func (c *Certificate) CheckSignatureFrom(parent *Certificate) error { func (c *Certificate) CheckSignatureFrom(parent *Certificate) error {
return c.checkSignatureFrom(parent, nil)
}
func (c *Certificate) checkSignatureFrom(parent *Certificate, opts *VerifyOptions) error {
// RFC 5280, 4.2.1.9: // RFC 5280, 4.2.1.9:
// "If the basic constraints extension is not present in a version 3 // "If the basic constraints extension is not present in a version 3
// certificate, or the extension is present but the cA boolean is not // certificate, or the extension is present but the cA boolean is not
@ -923,11 +938,7 @@ func (c *Certificate) CheckSignatureFrom(parent *Certificate) error {
return ConstraintViolationError{} return ConstraintViolationError{}
} }
if parent.PublicKeyAlgorithm == UnknownPublicKeyAlgorithm { return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature, parent.PublicKey, false, opts)
return ErrUnsupportedAlgorithm
}
return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature, parent.PublicKey, false)
} }
// CheckSignature verifies that signature is a valid signature over signed from // CheckSignature verifies that signature is a valid signature over signed from
@ -938,7 +949,7 @@ func (c *Certificate) CheckSignatureFrom(parent *Certificate) error {
// [MD5WithRSA] signatures are rejected, while [SHA1WithRSA] and [ECDSAWithSHA1] // [MD5WithRSA] signatures are rejected, while [SHA1WithRSA] and [ECDSAWithSHA1]
// signatures are currently accepted. // signatures are currently accepted.
func (c *Certificate) CheckSignature(algo SignatureAlgorithm, signed, signature []byte) error { func (c *Certificate) CheckSignature(algo SignatureAlgorithm, signed, signature []byte) error {
return checkSignature(algo, signed, signature, c.PublicKey, true) return checkSignature(algo, signed, signature, c.PublicKey, true, nil)
} }
func (c *Certificate) hasNameConstraints() bool { func (c *Certificate) hasNameConstraints() bool {
@ -960,10 +971,24 @@ func signaturePublicKeyAlgoMismatchError(expectedPubKeyAlgo PublicKeyAlgorithm,
// checkSignature verifies that signature is a valid signature over signed from // checkSignature verifies that signature is a valid signature over signed from
// a crypto.PublicKey. // a crypto.PublicKey.
func checkSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey, allowSHA1 bool) (err error) { func checkSignature(algo SignatureAlgorithm, signed, signature []byte, publicKey crypto.PublicKey, allowSHA1 bool, opts *VerifyOptions) (err error) {
var hashType crypto.Hash var hashType crypto.Hash
var pubKeyAlgo PublicKeyAlgorithm var pubKeyAlgo PublicKeyAlgorithm
if algo == UnknownSignatureAlgorithm {
pki, ok := publicKey.(*publicKeyInfo)
if !ok || opts == nil || opts.UnknownAlgorithmVerifier == nil {
return ErrUnsupportedAlgorithm
}
return opts.UnknownAlgorithmVerifier(
pki.Algorithm,
signed,
signature,
pki.PublicKey.Bytes,
)
}
for _, details := range signatureAlgorithmDetails { for _, details := range signatureAlgorithmDetails {
if details.algo == algo { if details.algo == algo {
hashType = details.hash hashType = details.hash
@ -1585,7 +1610,7 @@ func signTBS(tbs []byte, key crypto.Signer, sigAlg SignatureAlgorithm, rand io.R
} }
// Check the signature to ensure the crypto.Signer behaved correctly. // Check the signature to ensure the crypto.Signer behaved correctly.
if err := checkSignature(sigAlg, tbs, signature, key.Public(), true); err != nil { if err := checkSignature(sigAlg, tbs, signature, key.Public(), true, nil); err != nil {
return nil, fmt.Errorf("x509: signature returned by signer is invalid: %w", err) return nil, fmt.Errorf("x509: signature returned by signer is invalid: %w", err)
} }
@ -2259,7 +2284,7 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error
// CheckSignature reports whether the signature on c is valid. // CheckSignature reports whether the signature on c is valid.
func (c *CertificateRequest) CheckSignature() error { func (c *CertificateRequest) CheckSignature() error {
return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey, true) return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey, true, nil)
} }
// RevocationListEntry represents an entry in the revokedCertificates // RevocationListEntry represents an entry in the revokedCertificates