Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,70 @@ ruby examples/api_client/api_client.rb
See the link:examples/README.md[examples README] for complete documentation and
learning paths.

== XML Signature (Moxml::Signature)

Moxml includes an XML-implementation-agnostic implementation of W3C XML
Signature (xmldsig-core-1.1). Sign and verify documents with any moxml
adapter; the canonicalization engine produces byte-exact output that
cross-verifies with libxmlsec1.

=== Quick start

[source,ruby]
----
require "moxml"
require "moxml/signature"
require "openssl"

ctx = Moxml.new(:nokogiri)
key = OpenSSL::PKey::RSA.generate(2048)
doc = ctx.parse("<doc><greeting>Hello, World!</greeting></doc>")

# Sign
signature = Moxml::Signature.sign(
context: ctx, document: doc, key: key,
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
reference_uri: "",
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
)
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
doc.root.add_child(serialized.root)

# Verify (auto-extracts key from KeyInfo if absent)
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
result.valid? # => true
----

=== Supported algorithms

Digests:: SHA-1, SHA-224, SHA-256, SHA-384, SHA-512
Signature methods:: RSA-PKCS1v1.5, HMAC, ECDSA (P-256/P-384/P-521), DSA
Canonicalization:: Exclusive C14N 1.0, Inclusive C14N 1.0, Inclusive C14N 1.1
Transforms:: base64, Enveloped Signature

Adding a custom algorithm means declaring `identifier "http://..."` on a
subclass — no edits to existing code. See
link:docs/signature/algorithms.md[the algorithms doc].

=== Canonicalization (Moxml::C14n)

C14N is a top-level moxml feature (sibling to XPath, Builder, SAX).
Inclusive C14N is ported from the sibling `canon` gem; Exclusive C14N
is moxml-native. See link:docs/signature/c14n.md[the C14N doc].

=== Documentation

* link:docs/signature/architecture.md[Architecture]
* link:docs/signature/algorithms.md[Algorithm registry]
* link:docs/signature/c14n.md[Canonicalization]
* link:docs/signature/flows.md[Signer and Verifier flows]
* link:docs/signature/key-extraction.md[Key extraction] (TODO)
* link:docs/signature/security.md[Security considerations]
* link:docs/signature/quick-reference.md[Quick reference]
* link:examples/signature/[Runnable examples]

== Working with documents

=== Using the builder pattern
Expand Down
108 changes: 108 additions & 0 deletions docs/signature/algorithms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Algorithm registry

The `Moxml::Signature::Algorithms` module is the open-closed hub for
all W3C XML Signature algorithms. Algorithms are identified by URI;
the registry maps URI → class for each of four categories:

| Category | W3C spec section | Base class |
| -------------------- | ---------------- | --------------------------------------------- |
| `:digest` | §6.2 | `Algorithms::DigestBase` |
| `:signature_method` | §6.3, §6.4 | `Algorithms::SignatureMethodBase` |
| `:canonicalization` | §6.5 | `Algorithms::CanonicalizationBase` |
| `:transform` | §6.6 | `Algorithms::TransformBase` |

## Built-in algorithms

### Digests (§6.2)

| URI | Class |
| ---------------------------------------------------------- | ---------------- |
| `http://www.w3.org/2000/09/xmldsig#sha1` | `SHA1` |
| `http://www.w3.org/2001/04/xmldsig-more#sha224` | `SHA224` |
| `http://www.w3.org/2001/04/xmlenc#sha256` (REQUIRED) | `SHA256` |
| `http://www.w3.org/2001/04/xmldsig-more#sha384` | `SHA384` |
| `http://www.w3.org/2001/04/xmlenc#sha512` | `SHA512` |

### Signature methods (§6.3, §6.4)

| URI | Class | Notes |
| ---------------------------------------------------------- | ---------------- | ----- |
| `…xmldsig#rsa-sha1` | `RsaPkcs1Sha` | Verification only (BP: SHA-1 discouraged) |
| `…xmldsig-more#rsa-sha224` | `RsaPkcs1Sha` | |
| `…xmldsig-more#rsa-sha256` (REQUIRED) | `RsaPkcs1Sha` | |
| `…xmldsig-more#rsa-sha384` | `RsaPkcs1Sha` | |
| `…xmldsig-more#rsa-sha512` | `RsaPkcs1Sha` | |
| `…xmldsig#hmac-sha1` | `HmacSha` | Truncation enforced per §4.4.2 |
| `…xmldsig-more#hmac-sha224` | `HmacSha` | |
| `…xmldsig-more#hmac-sha256` (REQUIRED) | `HmacSha` | |
| `…xmldsig-more#hmac-sha384` | `HmacSha` | |
| `…xmldsig-more#hmac-sha512` | `HmacSha` | |
| `…xmldsig-more#ecdsa-sha1` | `EcdsaSha` | |
| `…xmldsig-more#ecdsa-sha224` | `EcdsaSha` | |
| `…xmldsig-more#ecdsa-sha256` (REQUIRED) | `EcdsaSha` | P-256/P-384/P-521 |
| `…xmldsig-more#ecdsa-sha384` | `EcdsaSha` | |
| `…xmldsig-more#ecdsa-sha512` | `EcdsaSha` | |
| `…xmldsig#dsa-sha1` | `DsaSha` | |
| `…xmldsig11#dsa-sha256` | `DsaSha` | |

### Canonicalization (§6.5)

| URI | Engine |
| ---------------------------------------------------------- | ---------------------------- |
| `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` | `Moxml::C14n::Inclusive10` (canon-ported) |
| `…REC-xml-c14n-20010315#WithComments` | same, `with_comments: true` |
| `http://www.w3.org/2006/12/xml-c14n11` | `Moxml::C14n::Inclusive11` |
| `…xml-c14n11#WithComments` | same, `with_comments: true` |
| `http://www.w3.org/2001/10/xml-exc-c14n#` | `Moxml::C14n::Exclusive` (moxml-native) |
| `…xml-exc-c14n#WithComments` | same, `with_comments: true` |

### Transforms (§6.6)

| URI | Class |
| ---------------------------------------------------------- | ------------------------------ |
| `http://www.w3.org/2000/09/xmldsig#base64` | `Base64Transform` |
| `http://www.w3.org/2000/09/xmldsig#enveloped-signature` | `EnvelopedSignatureTransform` |

Canonicalization algorithms can also be used as transforms per §6.6.1.
The `TransformPipeline` looks them up in the canonicalization registry
as a fallback.

## Adding a custom algorithm

```ruby
require "moxml/signature"

module MyAlgo
class SHA3_256 < Moxml::Signature::Algorithms::DigestBase
identifier "http://www.w3.org/2007/xmldsig-more#sha3-256"

def compute_digest(data)
OpenSSL::Digest.digest("SHA3-256", data)
end
end
end

# Now the URI resolves:
Moxml::Signature::Algorithms.lookup(
:digest,
"http://www.w3.org/2007/xmldsig-more#sha3-256",
)
# => MyAlgo::SHA3_256
```

The `identifier` declaration registers the class on load. No edits to
existing code are required — pure OCP.

## API

```ruby
Algorithms.lookup(:digest, uri) # → class, raises UnknownAlgorithm
Algorithms.registered?(:digest, uri) # → bool
Algorithms[:digest] # → { uri => class, ... }
```

## Lazy loading

The registry autoloads built-in algorithm classes on first lookup
(`load_builtins!` references each constant, triggering autoload).
Custom algorithms register themselves on `require` of their file.
148 changes: 148 additions & 0 deletions docs/signature/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Moxml::Signature — Architecture

## Where it lives

`Moxml::Signature` is a sub-module of moxml that implements W3C XML
Signature (xmldsig-core-1.1). It is **XML implementation agnostic** —
every XML operation flows through `Moxml::Document` / `Moxml::Element`,
so the same signature code works whether you parse with Nokogiri, Oga,
REXML, Ox, or LibXML.

C14N itself is a top-level `Moxml::C14n` feature, sibling to
`Moxml::XPath`, `Moxml::Builder`, and `Moxml::SAX`. Signature uses it;
so can any other consumer (e.g., the sibling `canon` gem).

## Module layout

```
lib/moxml.rb # top-level, autoloads Signature and C14n
lib/moxml/signature.rb # Signature namespace + .sign/.verify entry points

lib/moxml/signature/errors.rb # error hierarchy
lib/moxml/signature/algorithms.rb # OCP registry hub
lib/moxml/signature/algorithms/ # concrete algorithms (digests, sig methods, transforms)
lib/moxml/signature/model/ # PORO models
lib/moxml/signature/serializer.rb # model → XML (uses moxml primitives)
lib/moxml/signature/parser.rb # XML → model
lib/moxml/signature/reference_resolver.rb # Reference URI → node-set / octets
lib/moxml/signature/transform_pipeline.rb # shared transform chain (DRY)
lib/moxml/signature/signer.rb # spec §3.1 signing flow
lib/moxml/signature/verifier.rb # spec §3.2 verification flow
lib/moxml/signature/key_extractor.rb # X509 / RSA / DSA / EC / KeyName → OpenSSL key
lib/moxml/signature/verification_result.rb
lib/moxml/signature/single_verification_result.rb
lib/moxml/signature/reference_result.rb

lib/moxml/c14n.rb # top-level C14n namespace
lib/moxml/c14n/ # canon-ported engine + moxml-native Exclusive
```

## Layering

```
┌──────────────────────────────────────────────────────────────────┐
│ Application code │
│ Moxml::Signature.sign / .verify │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Orchestration: Signer, Verifier, TransformPipeline, KeyExtractor│
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ Algorithms (OCP hub) │ │ Models: Signature, SignedInfo, etc. │
└──────────────────────┘ └──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Moxml::C14n (canon-ported Inclusive + moxml-native Exclusive) │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Moxml::Document / Element / Text / Namespace / Attribute │
│ (adapter-agnostic — Nokogiri, Oga, REXML, Ox, LibXML) │
└──────────────────────────────────────────────────────────────────┘
OpenSSL (crypto)
```

## Core design decisions

### 1. Algorithm registry as the OCP hub

Every W3C algorithm (digest, signature method, canonicalization,
transform) is identified by URI. The `Moxml::Signature::Algorithms`
module is the registry; adding a new algorithm means:

1. Subclass the relevant base (`DigestBase`, `SignatureMethodBase`,
`CanonicalizationBase`, `TransformBase`).
2. Declare `identifier "http://..."` on the subclass.
3. Add an autoload entry in `algorithms.rb` and a reference in
`load_builtins!`.

No edits to existing code. The registry has four categories:
`:digest`, `:signature_method`, `:canonicalization`, `:transform`.

### 2. Models are POROs; serialization is a service

Models (`Model::Signature`, `Model::SignedInfo`, `Model::Reference`,
etc.) are plain Ruby objects with `attr_accessor`. They do **not** own
their wire shape. A dedicated `Serializer` translates model → XML using
moxml primitives; `Parser` translates XML → model. This keeps the data
shape and the wire shape independent, and matches the user's global
rule ("no hand-rolled serialization on model classes").

### 3. Signer / Verifier orchestrate, don't compute

`Signer` walks the references, delegates to the transform pipeline,
computes digests via `DigestMethod` instances, and signs the
canonicalized SignedInfo. It contains no algorithm-specific logic.

`Verifier` follows Best Practice 1: authenticate SignatureValue first,
then run reference transforms. Errors are captured into the result
object (`SingleVerificationResult#error`), not raised, so a malicious
signature cannot panic the application.

### 4. TransformPipeline is shared by Signer and Verifier

The transform-chain logic (lookup algorithm, coerce input type, apply,
repeat) is the same for signing and verifying. It lives in
`TransformPipeline` — DRY.

### 5. C14N is shared infrastructure

Inclusive C14N is ported from `~/src/lutaml/canon` (mature, ~1,200
lines, full node-set subset support, xml:base fixup, xml:* inheritable
attribute resolution). Exclusive C14N is moxml-native (canon doesn't
implement it). Both expose the same `#canonicalize(node, with_comments:,
inclusive_namespaces:)` interface.

## Adapter-agnostic invariant

Every XML operation — parse, walk, serialize, canonicalize — goes
through `Moxml::Node`. The signature module never imports Nokogiri,
Oga, REXML, Ox, or LibXML directly. Switching adapters does not change
signature behavior.

The one exception is the `context:` parameter threaded through every
constructor. When a transform receives octet-stream input, it parses
with the same adapter the caller used (`context.parse(...)`), preserving
byte-exact canonicalization across adapters.

## Cross-verification

`spec/fixtures/xmldsig/sign2-result.xml` and `sign3-result.xml` are
real libxmlsec1-produced signatures (from the Ruby
`nokogiri-xmlsec-instructure` reference). Both verify byte-exact against
`Moxml::Signature.verify`, proving the C14N and signing logic matches a
battle-tested C implementation.

## What this module deliberately doesn't do

- **XPath Filter transform** — Best Practice 5 says avoid. The Enveloped
Signature transform walks ancestors directly, no XPath needed.
- **XSLT transform** — Best Practice 3 says avoid. Disabled.
- **External URI dereferencing** — Best Practice 8 says constrain.
Applications must provide their own resolver.
- **X.509 chain validation** — application responsibility (trust policy).
- **XML Encryption** — separate spec (xmlenc-core-1.1).
- **XAdES** — separate spec (ETSI TS 101 903).
Loading
Loading