Skip to content

feat: Ed25519 signatures, XML Encryption 1.1, and crypto.Signer support#101

Closed
leifj wants to merge 6 commits intomoov-io:masterfrom
sirosfoundation:pr/ed25519-xmlenc-cryptosigner
Closed

feat: Ed25519 signatures, XML Encryption 1.1, and crypto.Signer support#101
leifj wants to merge 6 commits intomoov-io:masterfrom
sirosfoundation:pr/ed25519-xmlenc-cryptosigner

Conversation

@leifj
Copy link
Copy Markdown

@leifj leifj commented Mar 24, 2026

Summary

Add Ed25519 (EdDSA) signature support, a comprehensive XML Encryption 1.1 package, and refactor signing to use the standard crypto.Signer interface for HSM/PKCS#11 compatibility.

Ed25519 Signature Support

  • Register http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519 algorithm (RFC 9231 Section 2.3.1)
  • Ed25519 is a "pure" signature scheme — signs the message directly without pre-hashing
  • Signing and verification support with proper handling of the no-pre-hash requirement
  • Tests in ed25519_test.go

XML Encryption 1.1 Package (xmlenc/)

New xmlenc sub-package implementing W3C XML Encryption 1.1:

  • AES Key Wrap (RFC 3394): AES-128/192/256-KW with NIST test vectors
  • X25519 Key Agreement: ECDH-ES with Curve25519
  • HKDF Key Derivation (RFC 5869): HMAC-SHA256/384/512-based key derivation
  • Content Encryption: AES-128/192/256-GCM and AES-128/192/256-CBC
  • XML Types: Complete type system for EncryptedData, EncryptedKey, AgreementMethod, KeyDerivationMethod, OriginatorKeyInfo, RecipientKeyInfo
  • XML Serialization/Parsing: Compatible with W3C XML Encryption 1.1 namespace

EU eDelivery AS4 2.0 compatible: X25519 + HKDF-SHA256 + AES-128-KW + AES-128-GCM

Test coverage includes RFC 3394 NIST vectors, W3C interop test cases, and full round-trip tests.

crypto.Signer Refactor

Replace concrete key type assertions with the standard crypto.Signer interface:

  • RSA signing falls back to crypto.Signer when the key is not *rsa.PrivateKey
  • Ed25519 signing falls back to crypto.Signer with Hash(0) for no-pre-hash
  • Introduces P11SignerOpts implementing crypto.SignerOpts
  • Adds ProcessElement helper for canonicalizing elements with namespace context

This enables HSM/PKCS#11 signers (which implement crypto.Signer) without requiring a direct dependency on any PKCS#11 library. No CGO dependency is introduced.

Other Enhancements

  • Skip cid: URI references in digest calculation (MIME attachments in WS-Security)

Testing

All existing tests pass. New test files:

  • ed25519_test.go — Ed25519 sign/verify round-trip
  • xmlenc/*_test.go — Key wrap, key agreement, encryption, W3C interop

Leif Johansson added 3 commits March 24, 2026 10:48
This adds comprehensive XML Encryption 1.1 support including:

- AES Key Wrap (RFC 3394) - AES-128/192/256-KW
- X25519 Key Agreement with HKDF key derivation (RFC 5869)
- AES-GCM and AES-CBC content encryption
- Complete XML Encryption types: EncryptedData, EncryptedKey, AgreementMethod, KeyDerivationMethod
- XML serialization/parsing compatible with W3C XML Encryption 1.1

EU eDelivery AS4 2.0 Compatibility:
- X25519 key agreement
- HKDF key derivation with HMAC-SHA256
- AES-128-KW key wrapping
- AES-128-GCM content encryption

Also adds Ed25519 signature support for XML Digital Signatures.

Test coverage includes:
- RFC 3394 official NIST test vectors
- W3C XML Encryption interop test cases
- Full encryption/decryption round-trip tests
The parseAgreementMethod function was missing support for parsing
OriginatorKeyInfo and RecipientKeyInfo elements, which contain the
ephemeral public key needed for X25519 key agreement during decryption.

This fix enables full round-trip XML serialization/deserialization
of EncryptedData elements with X25519 key agreement.
Remove the hard dependency on crypto11/PKCS#11 by using the standard
crypto.Signer interface. This allows signedxml to work with:
- Standard Go private keys (*rsa.PrivateKey, ed25519.PrivateKey)
- PKCS#11/HSM signers (crypto11 implements crypto.Signer)
- Any other crypto.Signer implementation

This removes the CGO dependency from signedxml itself, making it
easier to build static binaries. PKCS#11 support is still available
when the caller provides a crypto11.Signer or any other crypto.Signer.

BREAKING CHANGE: None - crypto11.Signer embeds crypto.Signer, so
all existing code continues to work.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands the cryptographic capabilities of the library by integrating modern signature and encryption standards. The addition of Ed25519 signatures enhances security and interoperability, particularly for emerging protocols. Furthermore, the introduction of a dedicated XML Encryption 1.1 package provides robust data confidentiality features, supporting key agreement, key wrapping, and content encryption. A crucial architectural improvement is the adoption of the crypto.Signer interface, which future-proofs the signing process by allowing seamless integration with external cryptographic devices and services, without increasing the project's dependency footprint.

Highlights

  • Ed25519 Signature Support: Added comprehensive support for Ed25519 (EdDSA) XML digital signatures, including algorithm registration (http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519), signing, and verification, with specific handling for its 'pure' signature scheme (no pre-hashing).
  • XML Encryption 1.1 Package: Introduced a new xmlenc sub-package implementing W3C XML Encryption 1.1. This includes AES Key Wrap (RFC 3394), X25519 Key Agreement with HKDF key derivation (RFC 5869), and various content encryption algorithms (AES-GCM, AES-CBC). It provides a complete type system for XML Encryption elements and supports XML serialization/parsing, making it compatible with EU eDelivery AS4 2.0.
  • crypto.Signer Interface Refactor: Refactored the signing mechanism to leverage the standard crypto.Signer interface. This enhancement allows for greater flexibility, enabling compatibility with Hardware Security Modules (HSMs) and PKCS#11 implementations without introducing CGO dependencies. RSA and Ed25519 signing now gracefully fall back to this interface.
  • CID URI Reference Handling: Implemented logic to skip cid: URI references during digest calculation. This addresses a specific requirement for MIME attachments in WS-Security, where such references typically have pre-computed digests and use transforms not handled by the standard XML Signature processing.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new features: Ed25519 signature support, a comprehensive XML Encryption 1.1 implementation in a new xmlenc package, and a refactoring to use the crypto.Signer interface, which improves support for HSMs. The changes are extensive and well-tested, particularly the new xmlenc package which includes W3C interop and NIST test vectors.

My review has identified a few areas for improvement. The most important is updating the very old golang.org/x/crypto dependency to a recent version to mitigate potential security risks. Additionally, there are several places in the new XML parsing logic where errors from base64 decoding are ignored, which could lead to panics or incorrect behavior with malformed input. I've also noted a minor issue in a test file where an error is ignored.

Overall, this is a great contribution that adds valuable functionality. Addressing the identified issues will improve the robustness and security of the new code.

Comment thread go.mod Outdated
Comment thread xmlenc/types.go Outdated
Comment thread ed25519_test.go Outdated
leifj pushed a commit to sirosfoundation/signedxml that referenced this pull request Mar 24, 2026
Incorporates all review feedback from upstream PRs:
- PR moov-io#98 (c14n-fixes): WriteToString error handling, redundant assignment removal
- PR moov-io#99 (rsa-pss): getDigestAlgorithm helper, rsaPSSHashAlgorithms shared map
- PR moov-io#100 (ecdsa-fix): hash messages before ECDSA sign/verify in tests, nolint:exhaustive
- PR moov-io#101 (ed25519-xmlenc-cryptosigner): error handling in xmlenc parse functions,
  gosec nolint for safe integer conversions, NewSigner error check in tests,
  .gitleaks.toml for W3C test vectors

Also updates golang.org/x/crypto to v0.49.0.
@adamdecaf
Copy link
Copy Markdown
Member

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive support for XML Encryption 1.1, including AES-GCM, AES-CBC, AES Key Wrap, X25519 Key Agreement with HKDF key derivation, and Ed25519 XML signatures. It adds a new xmlenc package with encryption/decryption functionalities, key agreement mechanisms, and extensive W3C interoperability tests. Additionally, it updates the Go version, adds golang.org/x/crypto dependency, and improves XML canonicalization by handling inclusive namespaces and skipping external references. Feedback includes addressing a hardcoded SHA256 hash function in the HKDF implementation, defining URIs for HMAC-SHA384 and HMAC-SHA512, and clarifying a misleading comment about uint64 overflow in the key wrap logic.

Comment thread xmlenc/keyagreement.go
Comment on lines +89 to +100
hashFunc := sha256.New

var salt []byte
var info []byte

if params != nil {
salt = params.Salt
info = params.Info
if params.KeyLength > 0 {
keyLength = params.KeyLength / 8 // Convert bits to bytes
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The hashFunc for HKDF is hardcoded to sha256.New, which ignores the PRF field in HKDFParams. This prevents the use of other hash functions like HMAC-SHA384 or HMAC-SHA512. The implementation should select the hash function based on params.PRF.

You will also need to add "crypto/sha512" to the imports.

    hashFunc := sha256.New

	var salt []byte
	var info []byte

	if params != nil {
		if params.PRF != "" {
			switch params.PRF {
			case AlgorithmHMACSHA256:
				hashFunc = sha256.New
			case AlgorithmHMACSHA384:
				hashFunc = sha512.New384
			case AlgorithmHMACSHA512:
				hashFunc = sha512.New
			default:
				return nil, fmt.Errorf("unsupported PRF algorithm: %s", params.PRF)
			}
		}
		salt = params.Salt
		info = params.Info
		if params.KeyLength > 0 {
			keyLength = params.KeyLength / 8 // Convert bits to bytes
		}
	}

Comment thread xmlenc/algorithms.go
AlgorithmSHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
AlgorithmSHA384 = "http://www.w3.org/2001/04/xmlenc#sha384"
AlgorithmSHA512 = "http://www.w3.org/2001/04/xmlenc#sha512"
AlgorithmHMACSHA256 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To fully support HMAC-SHA384 and HMAC-SHA512 for HKDF as mentioned in the PR description, you should also define their algorithm URIs here. This will allow the key derivation logic to correctly select the hash function.

Suggested change
AlgorithmHMACSHA256 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"
AlgorithmHMACSHA256 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"
AlgorithmHMACSHA384 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384"
AlgorithmHMACSHA512 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"

Comment thread xmlenc/keywrap.go
for j := 5; j >= 0; j-- {
for i := n; i >= 1; i-- {
// t = n*j+i
t := uint64(n*j + i) //nolint:gosec // n<=6, j<=5, i<=n; overflow impossible
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment // n<=6, j<=5, i<=n; overflow impossible is misleading. The value of n is derived from the ciphertext length and is not limited to 6. For example, wrapping a 512-bit key would result in n=8. While an overflow of t is practically unlikely for typical key sizes, the comment should be corrected to avoid confusion. A better explanation would be that uint64 is large enough for practical values of n.

@adamdecaf
Copy link
Copy Markdown
Member

Do you want to fixup this PR's merge conflicts and address the hardcoded HKDF?

@ubavic
Copy link
Copy Markdown
Contributor

ubavic commented Mar 28, 2026

This PR brings a refactor that I’ve been thinking about proposing for a long time. Thank you for making this :)

I would suggest just one thing: the type of the privateKey field in the Signer structure could now be changed from any to crypto.Signer, and consequently Sign(privateKey interface{}) could be updated to Sign(privateKey crypto.Signer).

Also, if I am not mistaken, the majority of the logic in setSignature could be replaced by using the crypto.Signer.Sign implementation, since both rsa.PrivateKey and ed25519.PrivateKey implement crypto.Signer (and those implementation cover logic that is written in this PR).

@adamdecaf
Copy link
Copy Markdown
Member

adamdecaf commented Apr 2, 2026

Thanks @leifj - I've included these commits on master - couldn't push to this PR, but the tests pass.

f22487e...1efeb37

@adamdecaf adamdecaf closed this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants