feat(pqsign): WASM signer for browser-side post-quantum identity
WebCrypto has Ed25519 but no ML-DSA, so the post-quantum half is compiled from the same pkg/pqid the server verifies with. One implementation of the scheme in the project means a client and server cannot disagree about signing. The Ed25519 half is stored as its 32-byte seed rather than the expanded key, since the seed cannot encode an inconsistent pair, and the public key is derived rather than stored so a client cannot present one that does not match what it signs with. Verified end to end in a JS runtime: 1984-byte public key, 3373-byte signature, derived key matches, malformed input returns an error rather than crashing the module. 3.4MB, 0.9MB gzipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,3 +169,40 @@ func Verify(pub *PublicKey, msg, sig []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrivateFromBytes reconstructs a private key from stored material.
|
||||
//
|
||||
// The Ed25519 half is stored as its 32-byte seed rather than the expanded
|
||||
// key, because the seed is the canonical form and cannot encode an
|
||||
// inconsistent pair. The ML-DSA half is stored in its own binary encoding.
|
||||
func PrivateFromBytes(edSeed, pqKey []byte) (*PrivateKey, error) {
|
||||
if len(edSeed) != ed25519.SeedSize {
|
||||
return nil, fmt.Errorf("%w: ed25519 seed is %d bytes, want %d",
|
||||
ErrMalformedKey, len(edSeed), ed25519.SeedSize)
|
||||
}
|
||||
var pq mldsa65.PrivateKey
|
||||
if err := pq.UnmarshalBinary(pqKey); err != nil {
|
||||
return nil, fmt.Errorf("%w: ML-DSA private key: %v", ErrMalformedKey, err)
|
||||
}
|
||||
return &PrivateKey{
|
||||
Ed: ed25519.NewKeyFromSeed(edSeed),
|
||||
PQ: &pq,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublicFromPrivate derives the public half.
|
||||
//
|
||||
// Deriving rather than storing means a client cannot present a public key that
|
||||
// does not match the key it signs with — a mismatch that would otherwise only
|
||||
// surface as a confusing authentication failure.
|
||||
func PublicFromPrivate(priv *PrivateKey) (*PublicKey, error) {
|
||||
edPub, ok := priv.Ed.Public().(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: ed25519 private key has no public half", ErrMalformedKey)
|
||||
}
|
||||
pqPub, ok := priv.PQ.Public().(*mldsa65.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: ML-DSA private key has no public half", ErrMalformedKey)
|
||||
}
|
||||
return &PublicKey{Ed: edPub, PQ: pqPub}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user