xmlenc1

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 31 Imported by: 0

README

xmlenc1

The xmlenc1 package implements W3C XML Encryption 1.1 for helium documents. What it declines to implement is the cryptography the standards bodies have since retired: Triple DES is refused, because NIST disallowed it for encryption after 2023. Conformance scope states that refusal and its reason, and names every construct this package does not read.

Import path: github.com/lestrrat-go/helium/xmlenc1

Security

  • Secure by default. Encryptor defaults to authenticated AES-256-GCM under the XML Encryption 1.1 identifier AES256GCM11 (DefaultBlockAlgorithm) when no BlockAlgorithm is set. W3C xmlenc-core1 §5.2.4 defines AES-GCM only in the XML Encryption 1.1 namespace http://www.w3.org/2009/xmlenc11#, so that is the namespace a peer can recognize it in; its §5.1.1 table then marks aes128-gcm REQUIRED and aes256-gcm OPTIONAL, so the default trades a guarantee of support for the longer key and BlockAlgorithm(AES128GCM11) is the identifier every conforming peer must accept. 1.1 GCM uses the specified IV, ciphertext, and authentication-tag encoding without additional authenticated data.
  • The AES128GCM and AES256GCM identifiers, which put GCM in the 2001 XML Encryption namespace, are defined by no XML Security specification. Encryptor.BlockAlgorithm accepts them and Decryptor decrypts them, so every document this package has emitted keeps decrypting, but a conforming peer will not accept one. For those two identifiers the package binds the EncryptionMethod/@Algorithm URI into the AEAD additional authenticated data — this package's own measure against an on-the-wire algorithm substitution, not conformance with any specification, and a second reason a document carrying them does not interoperate.
  • An EncryptedData that carries no EncryptionMethod is decryptable only as an opt-in. W3C xmlenc-core1 §3.1 and §3.2 leave the element optional and require the recipient to already know the algorithm, and §4.4 admits obtaining it out of band, so Decryptor.BlockAlgorithm supplies the block algorithm URI; without it such a document fails with ErrMalformedEncrypted. The match against the document is strict, so setting it can only narrow what a decrypt accepts: the URI set there is used when the document declares none, the document's is used when none is set, and a pair that disagrees fails with ErrConflictingBlockAlgorithm. A document can therefore never override a caller who stated the algorithm out of band. Whichever URI the resolution returns is what the CBC opt-in, the legacy GCM additional authenticated data, and every key-length binding act on.
  • AES-CBC is unauthenticated and vulnerable to padding-oracle attacks (Jager/Somorovsky 2011).
    • Encryption: selecting a CBC BlockAlgorithm requires Encryptor.AllowLegacyCBC(true); otherwise encryption returns ErrCBCEncryptionRequiresOptIn. Opt in only to produce ciphertext for a legacy recipient that cannot accept AES-GCM.
    • Decryption: Decryptor refuses CBC by default and returns ErrCBCRequiresOptIn. Pass AllowUnauthenticatedCBC(true) only if you must accept legacy CBC and you have verified that decryption errors are not exposed to remote attackers. Decryption is the exposed operation: the attack feeds modified ciphertext to something that decrypts it and reads the answer, so a decryptor that accepts CBC is the oracle, while emitting CBC only leaves ciphertext for some other recipient to accept. Hiding the errors narrows that oracle without closing it — xmlenc-core1 §6.1.1 notes the surrounding protocol can signal well-formedness by itself — so this package collapses nearly every CBC failure to one ErrDecryptionFailed value and message, and AES-GCM remains the only full answer. The one exception is a wrong-length pre-shared SessionKey: the caller configures its length and an attacker cannot influence it, so a mismatch is reported as a bare KeySizeError instead. The oracle is the outcome, not the error text: under CBC, Decrypt succeeds only when the recovered plaintext parses, so its success or failure is the well-formedness oracle itself. The exact predicate depends on @Type: a Content payload need only parse as a well-formed fragment, while an Element payload must in addition parse to exactly one node, and that node must be an element. Any other @Type never reaches a parse at all. DecryptBytes is stronger still — it returns the plaintext octets on valid padding alone, with no XML constraint at all.
    • Padding: decryption reads the padding the way xmlenc-core1 §5.2.1 writes it. A plaintext short by N octets is padded with N-1 octets of ARBITRARY value and a final octet N, so the final octet is the only one a conforming decryptor may act on; this package checks that it names between one octet and one whole block, and does not read the rest. PKCS#7 fixes every one of those octets to N instead, which is a strict subset, so PKCS#7-padded ciphertext is always accepted here and the reverse does not hold. Decryptor.StrictPKCS7Padding(true) narrows the check to the PKCS#7 rule, and its godoc owns what that trades: it refuses conforming documents, so it suits only a caller that controls both ends. Whichever rule is in force, a refused padding reports the same ErrDecryptionFailed as every other CBC failure. Encryptor always writes PKCS#7-shaped padding, which both rules accept, so a document this package produced decrypts under either.
  • The inner parser used on the decrypted plaintext has DTD loading, external entity resolution, and network access all disabled. Decrypted bytes are attacker-controlled, so a relaxed parser would constitute an XXE oracle.
  • XML Encryption 1.1 ECDH-ES works in both directions with P-256, P-384, or P-521 and ConcatKDF. Decryptor.ECPrivateKey decrypts; Encryptor.RecipientECPublicKey with a KeyWrapAlgorithm encrypts. The key-encryption key is derived, not supplied, so KeyEncryptionKey belongs to the separate AES key wrapping mechanism; a fresh ephemeral key pair is generated per encryption and only its public half travels, in the xenc:AgreementMethod. KeyDerivationParams sets the ConcatKDF parameters, which are written to the wire because both sides must derive with identical values.
  • The five ConcatKDF OtherInfo fields are limited to 4096 bytes together, because they arrive in an attacker-supplied document and drive work proportional to their size. Real OtherInfo is identifiers and nonces, so the limit is far above any interoperable value; over it fails with ErrMalformedEncrypted, when parsing a document and when deriving from parameters a caller built directly. Parameters with an empty DigestMethod are the one set that is never measured: they fall back to SHA-256 with empty OtherInfo, which discards the caller's fields before any derivation. ConcatKDFParams' godoc owns this rule.
  • An xenc:OAEPparams element is limited to 1 KiB decoded, on the EncryptedData's own EncryptionMethod and on every EncryptedKey's alike, because both are read before any key is resolved and before anything the document says has been authenticated. The element carries the RSA-OAEP label, which is hashed before use and is a handful of octets in practice; over the limit fails with ErrMalformedEncrypted. The same limit applies to Encryptor.OAEPParams, where a larger label fails the encryption with ErrEncryptionFailed before any payload work — and only when key transport is the mechanism in use, since that is the only one that writes a label — so a label this package writes is a label it reads back. The limit is a policy ceiling, and no conformance boundary requires it: the xenc schema puts no length facet on the element, so a larger label is valid, and this package intentionally refuses one in both directions, neither writing nor reading it. The value is weighed as it is read and never joined into one string, so what the parse keeps is sized by the limit no matter how much whitespace or how many CDATA sections a label is spread over. Only character data is read: a text or CDATA child, or an entity reference, which contributes its entity's declared replacement text and is not expanded any further. An element child is refused with the same error, because asking one for its content would pull in its whole subtree, and a comment or processing instruction is ignored, contributing nothing to the base64. The one cost that still follows the document is the copy the DOM hands out per child, which the walk pays exactly once.
  • A dsig11:PublicKey inside an ECDH-ES originator key is limited to 133 bytes decoded, because it is read while the document is parsed and the curve is otherwise the only thing that would refuse an oversized value — after the whole of it has been materialized. 133 is not a policy choice: it is the largest SEC1 uncompressed point the three supported curves encode (65 on P-256, 97 on P-384, 133 on P-521), and crypto/ecdh accepts nothing else, so a longer value is rejected either way. The limit is the maximum across all three, and the selected curve's own size cannot serve, because dsig11:NamedCurve may follow the dsig11:PublicKey or be absent altogether, so there may be no curve to size the value by when it is weighed. Over the limit fails with ErrMalformedEncrypted. The value is weighed as it is read and never joined into one string, so what the parse keeps is sized by the limit no matter how much whitespace or how many CDATA sections a point is spread over, and the same child rules as xenc:OAEPparams apply: only character data is read, an element child is refused, and a comment or processing instruction is ignored.
  • Encryptor.EncryptBytes and Decryptor.DecryptBytes handle payloads that are neither an element nor element content. EncryptBytes returns a detached EncryptedData with no Type attribute and does not modify the tree; recover this payload with DecryptBytes, which returns the plaintext octets without parsing them as XML. Decrypt parses only TypeElement and TypeContent and refuses every other Type — absent, empty, or an unrecognized URI — with ErrOpaquePayload. @Type sits outside the ciphertext and no block algorithm authenticates it, so treating an absent value as TypeElement would let anyone who can edit the document delete the attribute and have opaque plaintext parsed as XML and handed back as nodes to graft into a tree. xmlenc-core1 §4.2 asks a decryptor to read an unknown or empty Type as a signal that the cleartext is an opaque octet stream, and §3.1 puts the absent case in the same position. The error is deliberately not ErrMalformedEncrypted: such a document is well-formed, and a caller must be able to tell "retry with DecryptBytes" from a document to reject.
  • Decryptor.MaxEncryptedKeys caps how many <EncryptedKey> candidates are trial-decrypted (default 100, negative for unlimited), because an unbounded count is a CPU amplification vector; over the cap fails while parsing, before the excess candidate is parsed, retained, or reaches candidate crypto. A candidate a ds:RetrievalMethod supplies costs a slot exactly as an inline <EncryptedKey> does, and the slot is charged when the candidate is retained, so several references naming one <EncryptedKey> cost one slot between them and a reference that supplies no candidate costs none — only a probe of the id index the decrypt builds once. Its godoc owns the per-candidate branch dispatch: which key a candidate uses and what it costs. The cap is applied before the key configuration is consulted, so it also bounds a decrypt driven by a pre-shared Decryptor.SessionKey.
  • Decryptor.MaxEncryptedKeyBytes caps the total <EncryptedKey> ciphertext those candidates may carry together (default 64 KiB, negative for unlimited), because a count alone does not bound their size; over the budget fails with ErrEncryptedKeyBytesExceeded. Its godoc owns what the budget covers and when it is charged. The budget is charged while the document is read, so it too bounds a decrypt driven by a pre-shared SessionKey.
  • Decryptor.MaxCipherValueBytes caps the decoded <EncryptedData> payload one decrypt will hold (default 10 MiB, negative for unlimited), because the payload is the one value a document may make arbitrarily large; over the budget fails with ErrCipherValueBytesExceeded before the value is assembled or decoded, and so before block decryption or any plaintext parse. Its godoc owns what the budget covers and when it is charged. The default matches helium's own per-node content limit, which a payload spread over several text or CDATA nodes would otherwise slip past; this budget measures decoded octets, so neither that splitting nor the whitespace xs:base64Binary permits changes what it charges. It is charged while the document is read, so it too bounds a decrypt driven by a pre-shared SessionKey, and it is separate from MaxEncryptedKeyBytes, which bounds only the wrapped session-key candidates.
  • An xenc:CipherReference naming a resource OUTSIDE the document is denied by default. The four same-document forms need no I/O and always resolve; every other URI fails with ErrReferenceNotFound until the caller sets Decryptor.CipherReferenceResolver, and configuring one adds the external form without changing how any same-document form resolves. helium ships one implementation, FSReferenceResolver(fsys, root), which performs no network access and is fail-closed on anything that is not a plain in-tree path: a URI carrying an RFC 3986 scheme (a Windows drive letter included), a leftover fragment, and a path escaping the root after cleaning are all refused. root declares the document-space prefix fsys stands for, which is what lets an absolute URI inside that space resolve while one outside it stays refused; an empty root serves relative URIs only. No HTTP resolver ships, because an attacker who controls a CipherReference URI would otherwise steer requests at internal hosts or stall a decrypt, so whoever wants network dereferencing owns that SSRF and availability risk. A resolver hands back a stream and this package reads it, so the bound holds for a resolver a caller writes and not only for the shipped one: the read stops one byte past what the budget still allows — MaxCipherValueBytes for a payload reference, MaxEncryptedKeyBytes for a key one — it stops when the caller's context is done, and the stream is closed on every one of those paths. Be plain about the limit of that: it removes the PACKAGE's complicity, and it does not reach inside a resolver. A resolver that buffers a whole resource itself, or that blocks before returning a stream at all, does so on the caller's own account — nothing outside a resolver can constrain what happens within it. The division falls there because the two sides are trusted differently: the resolver is code the caller chose, while the URI is chosen by an unauthenticated document, and it is the URI the package must be immune to. A same-document reference is bounded by the same budgets, and the bound is on canonical OUTPUT OCTETS, not on the work producing them costs: the writer feeding c14n stops at the first byte past the allowance, but Canonical XML's own per-element scan for which namespace declarations are still in scope runs in proportion to elements times in-scope declarations, and a document shaped to be heavy in both can spend a great deal of that work while emitting almost nothing, leaving the byte budget with nothing to refuse. What actually bounds a resolution shaped that way is the caller's own context: the writer polls it on every write alongside the budget check, so a cancelled or expired caller stops the canonicalization promptly regardless of how little it has written so far. For a NAMED reference (#id or #xpointer(id('id'))), a node-set collector runs ahead of that writer and is itself bounded and linear in the subtree — linear in its elements, never in the product of its elements and the namespace declarations in scope on them, and a selection whose element count alone cannot fit the allowance is refused before it is built — and it too observes the caller's context once per node. The two WHOLE-DOCUMENT forms (URI="" and #xpointer(/) naming the document element) have no collector stage at all: canonicalizing straight to the writer is the only work such a reference does, so that writer's own context poll is the sole place the whole resolution can ever notice a caller who has stopped waiting.
  • A CipherReference may declare transforms, and only the XMLDSig #base64 transform is accepted. Every declared algorithm is validated before any of them runs, so a supported one standing ahead of an unsupported one is not executed first; a list longer than four, or a second xenc:Transforms, is refused unread. Neither xenc:CipherReferenceType nor xenc:TransformsType carries a wildcard, so an element the schema does not declare — a Transform in a foreign namespace, a Transforms wrapper in the wrong one — is refused outright, and counts against that cap as it is read: a namespace-shifted transform cannot hide from the whitelist, or from a policy layer reading the list. Refusing is conforming — xmlenc-core1 §3.3.1 marks both the Transform feature and the particular algorithms OPTIONAL — and XPath and XSLT are the reason the rule exists: either one evaluates an expression the document chose over a document nothing has authenticated yet, which is unbounded compute bought with a few bytes of markup. An unsupported algorithm fails with ErrMalformedEncrypted wrapping an *UnsupportedAlgorithmError that names the refused URI.
  • An AES key-wrap <EncryptedKey> must carry exactly the session-key length the declared block algorithm requires, plus RFC 3394's 8-byte integrity block. Any other length fails with ErrKeyUnwrapFailed before the unwrap rounds run, so a document cannot spend AES work on a ciphertext that is provably not a wrap of the key it declares. The length check belongs to the unwrap, so it is one of the steps a pre-shared Decryptor.SessionKey returns ahead of: that candidate is never resolved, and a document carrying one still decrypts.

Conformance scope

This package implements W3C xmlenc-core1, and where it deliberately departs from the specification it names the departure and the reason for it. One algorithm the specification marks REQUIRED is refused, and the refusal is deliberate and permanent:

  • Triple DES#tripledes-cbc (§5.2.2, REQUIRED) and #kw-tripledes (§5.7.1, REQUIRED) — refused deliberately, and this package will not implement them. Triple DES is a 64-bit block cipher, so Sweet32 (CVE-2016-2183) applies, and NIST SP 800-131A Rev. 2 disallows TDEA encryption after 2023. The specification marks both REQUIRED because it predates that retirement; this package follows the retirement. Block encryption and key wrapping are AES only. #tripledes-cbc names the block cipher and fails with an error matching the relevant operation sentinel while preserving *UnsupportedAlgorithmError, in every key configuration, a pre-shared SessionKey included; the CBC opt-in gate names only the two AES-CBC URIs, so AllowUnauthenticatedCBC(true) is neither required for that error nor changes it. The one case that reports something else is an EncryptedData carrying no <EncryptedKey> and no SessionKey: the missing key is checked first, so it fails with ErrMissingKey. #kw-tripledes names an <EncryptedKey>'s wrapping and fails the same way only when that key must be resolved, so a pre-shared SessionKey decrypts past it.

xenc:CipherReference (§3.3.1, REQUIRED) is implemented, on an EncryptedData payload and on every EncryptedKey alike. CipherData carries either a CipherValue holding the cipher text inline or a CipherReference naming it by URI, and the two are read into the same octets. Encryptor writes no CipherReference: §3.3.1 requires support for reading one, not for writing one.

§3.3.1 defines no dereferencing of its own — it requires "the same URI encoding, dereferencing, scheme, and HTTP response codes as that of [XMLDSIG-CORE1]" — so what is REQUIRED is what that model makes required. There, dereferencing URIs in the HTTP scheme is RECOMMENDED (xmldsig-core1 §4.4.3.1) while the null URI, the shortname XPointer, and same-document dereferencing are MUSTs (§4.4.3.2, §4.4.3.3). So the same-document forms are implemented unconditionally, with no setting to turn them off, and external URIs are default-denied behind an opt-in resolver.

The same four forms ds:RetrievalMethod recognizes resolve here: URI="", URI="#id", URI="#xpointer(/)", and URI="#xpointer(id('id'))". A present but empty @URI is the null URI naming the whole document; an absent @URI is a different thing entirely and fails with ErrMalformedEncrypted, because the xenc schema marks the attribute required. A URI matching more than one element fails with ErrAmbiguousReference and one matching none with ErrReferenceNotFound, for the same reasons those two refusals exist for ds:RetrievalMethod.

What a same-document reference names is a node-set, and how it becomes octets depends on the transforms:

  • with no transform, the node-set is converted by Canonical XML 1.0 without comments, which is what §4.4.3.3 requires of a node-set that has to become an octet stream. A whole-document form naming the document element canonicalizes the document, so the top-level processing instructions outside that element are included; every other form canonicalizes the named element's subtree.
  • with a #base64 transform first, that transform consumes the node-set directly and decodes its string-value: the text nodes of the selection in document order, concatenated. xmldsig-core1 §6.6.2 "strips away the start and end tags of the identified element and any of its descendant elements", so base64 written in a descendant, or split across an element boundary, decodes as the same characters written directly under the target would. The value is counted before it is built, through the same bounded walk an inline CipherValue goes through.

An external URI is joined against the base URI in force at the CipherReference element that wrote it — the document's own URL, narrowed by every xml:base on the way down to that element — and the joined URI is what reaches Decryptor.CipherReferenceResolver. A resolver therefore never sees the raw attribute and never performs the join. Its result is an octet stream, so no canonicalization applies to it. With no resolver configured it fails with ErrReferenceNotFound.

A document read with Parser.ParseFile carries that file's absolute path as its URL, so a sibling cipher text written as URI="ct.bin" arrives at the resolver as an absolute path in the document's space. That is what the root argument of FSReferenceResolver is for: FSReferenceResolver(os.DirFS("/srv/docs"), "/srv/docs") serves /srv/docs/ct.bin as ct.bin, and refuses every absolute path outside /srv/docs.

A transform list may declare up to four transforms and every one of them must be #base64; each one past the first decodes the octets the one before it produced. Security owns why nothing else is accepted there.

The resolved octets are cipher text and go straight to the block decryption; they are never re-parsed as a document. A CipherReference naming the EncryptedData that carries it, or naming itself, is therefore inert rather than recursive — it terminates with whatever those octets decrypt to — and there is no recursion depth to configure. Every one of these outcomes is decided while the document is read, so all of them precede the block-algorithm resolution, the AES-CBC opt-in gate, and a pre-shared Decryptor.SessionKey's early return: that caller does not decrypt past a reference this package refused, and a document this package will later refuse for its block algorithm still has its reference resolved first.

Same-document ds:RetrievalMethod (§3.5, REQUIRED) is implemented and always on, with no setting to turn it off. Inside a ds:KeyInfo, a <ds:RetrievalMethod Type=".../EncryptedKey" URI="#id"/> names the xenc:EncryptedKey holding the session key, wherever in the same document that key sits, and §3.5.3 permits several of them. The candidate it supplies is tried at the position the reference occupies, so a ds:KeyInfo mixing inline xenc:EncryptedKey children with references tries them in document order. Two references naming one EncryptedKey yield one candidate, decrypted once and charged once against both MaxEncryptedKeys and MaxEncryptedKeyBytes. Only the same-document form is REQUIRED and no external form is mandated anywhere, so a URI naming another resource is refused with ErrReferenceNotFound whatever it names — an external key location decides which key material the recipient trial-decrypts, which is not a decision a document gets to make for a caller. The four recognized forms are the ones XMLDSig core defines: URI="", URI="#id", URI="#xpointer(/)", and URI="#xpointer(id('id'))".

Two refusals are worth naming. A URI matching MORE than one element fails with ErrAmbiguousReference, and resolves to neither: an attacker who can inject an element carrying an Id already in use would otherwise choose which key the recipient unwraps, which is XML Signature Wrapping applied to encryption. A URI matching none fails with ErrReferenceNotFound. Both are decided while the document is read, so they precede a pre-shared Decryptor.SessionKey's early return: that caller does not decrypt past a reference this package refused.

A ds:RetrievalMethod must carry its URI attribute. A missing attribute is ErrMalformedEncrypted, even when the Type is one this package skips and even when a pre-shared SessionKey is configured. A present empty value is the valid null same-document URI. After that presence check, a Type this package does not implement — a #DerivedKey (§3.5.2), or any type from another specification — is stepped over before its URI is resolved, so it costs nothing, cannot fail a decrypt, and a pre-shared SessionKey decrypts past it. A Type of #EncryptedKey naming something that is not an xenc:EncryptedKey is a contradiction inside the document and fails with ErrMalformedEncrypted; a reference with no Type at all is resolved, and its target used only if it is an xenc:EncryptedKey. Encryptor writes no ds:RetrievalMethod: §3.5 requires support for reading one, not for writing one.

The other ds:KeyInfo children this package does not read are ds:KeyValue (§3.5, OPTIONAL), ds:KeyName (§3.5, RECOMMENDED), xenc11:DerivedKey (§3.5.2), and an xenc:AgreementMethod in the EncryptedData-level ds:KeyInfo position §5.6 defines for it. §3.5 marks AgreementMethod support OPTIONAL there, the same grade as ds:KeyValue. A ds:KeyValue inside an xenc:OriginatorKeyInfo is a different position and is read, since that is where ECDH-ES carries the sender's ephemeral key, and so is an xenc:AgreementMethod inside an xenc:EncryptedKey's own ds:KeyInfo, which this package DOES read — that is how it does ECDH-ES key agreement. An EncryptedData whose own ds:KeyInfo carries only the outer, EncryptedData-level form fails decryption with ErrMissingKey, and still decrypts under a pre-shared Decryptor.SessionKey, the same as any other document with no readable key candidate.

An xenc:KeySize child of EncryptionMethod is read and checked, but it is never used as a key length: every algorithm URI this package implements already fixes its own. A KeySize under a URI that implies a length must state exactly that length — in bits, per §5.6.2.2 — or the document is refused with ErrMalformedEncrypted while it is read, ahead of a pre-shared Decryptor.SessionKey's early return, so a contradicting value cannot be bypassed that way; an EncryptedData carrying no EncryptionMethod at all has no KeySize to check either way. A KeySize under a URI that implies no length — RSA key transport above all — is accepted and ignored. What remains absent is KeySize as the length SOURCE, used when the URI alone cannot supply one; that case arises only for stream ciphers and key agreements naming such an algorithm, none of which this package implements. Encryptor writes no KeySize.

The parser enforces the XML Encryption §3.2 EncryptionMethod child rules before decoding any child value. xenc:KeySize is allowed for every algorithm; ds:DigestMethod and xenc:OAEPparams are allowed only for RSA-OAEP; and xenc11:MGF is allowed only for RSA-OAEP 1.1. Unknown or misplaced direct children fail with ErrMalformedEncrypted, including when a pre-shared Decryptor.SessionKey would otherwise bypass key processing.

Section 3 carries a blanket "features described in this section MUST be implemented", so four more constructs are unimplemented, and only one of them can fail a decrypt: xenc:ReferenceList (§3.6), which points from a key to the items it encrypted and so only matters for a detached key this package cannot follow anyway; xenc11:DerivedKey (§3.5.2), which may appear in an EncryptedData's own ds:KeyInfo and tells the recipient to derive the content key from master key material it already holds — the parse reads no such key, so an EncryptedData offering ONLY that fails with ErrUnsupportedKeyDerivation, naming the facility this package lacks. That sentinel is deliberately not ErrMissingKey: no key the caller supplies can make such a document decrypt, so the error must not send it to audit its key configuration. Both ways a ds:KeyInfo offers the construct reach it, carried inline and named by a ds:RetrievalMethod whose Type says #DerivedKey. A document that ALSO carries a usable xenc:EncryptedKey decrypts under that key, since a construct this package does not implement costs it nothing it could otherwise do. The refusal is what the caller sees only once the block algorithm has resolved: resolution and the AES-CBC opt-in both run ahead of the key-resolution check, so an EncryptedData with no EncryptionMethod and no Decryptor.BlockAlgorithm fails with ErrMalformedEncrypted, and an AES-CBC one without AllowUnauthenticatedCBC(true) with ErrCBCRequiresOptIn, whatever its ds:KeyInfo holds. It does not arise at all when the caller supplies the key as a pre-shared SessionKey, whose early return precedes key resolution; xenc:CarriedKeyName (§3.5.1), which the parse steps over unread; and xenc:EncryptionProperties (§3.7), which is advisory metadata.

Choosing how the session key is protected

The content is always encrypted under a symmetric session key. What differs is how the recipient obtains that key:

Configuration Wire result
KeyTransportAlgorithm + RecipientPublicKey <EncryptedKey> holding the session key under RSA-OAEP
KeyWrapAlgorithm + RecipientECPublicKey <EncryptedKey> holding the session key under AES Key Wrap, with the wrapping key derived by ECDH-ES
KeyWrapAlgorithm + KeyEncryptionKey <EncryptedKey> holding the session key under AES Key Wrap (RFC 3394)
non-empty SessionKey alone no <EncryptedKey>; the recipient must already hold the key
none of the above ErrMissingConfig — nothing can protect the session key

The first three rows are mechanisms, and configuring two of them fails with ErrConflictingKeyConfig; its godoc owns that rule and why a single <EncryptedKey> makes it necessary. A SessionKey alongside one mechanism is allowed — it is the key that mechanism protects.

A non-empty SessionKey must match the block algorithm's key length exactly, else KeySizeError. An empty or nil SessionKey counts as not set: encryption generates a random key of the right length instead, so it never hits the length check.

Decrypting with a pre-shared session key

A non-empty Decryptor.SessionKey is not a preference among keys; it is an early return. Decrypt and DecryptBytes take it as the session key and return before candidate selection, per-candidate validation, and per-candidate key resolution. Its godoc owns the account of what that skips. It returns early from the key handling alone, not from the decrypt, so everything ahead of that point still holds: the MaxEncryptedKeys count, the MaxEncryptedKeyBytes budget, and the MaxCipherValueBytes payload budget, all charged while the document is read; Decrypt's @Type check (DecryptBytes does not read @Type, so it has no such gate); the block-algorithm resolution ErrMalformedEncrypted and ErrConflictingBlockAlgorithm come out of; the AES-CBC opt-in gate; and the check binding the supplied key's length to the resolved algorithm (KeySizeError). A decrypt that fails any of those reports that failure, whatever key the caller holds.

Decryption does not modify the tree

EncryptElement and EncryptContent splice <EncryptedData> into the document. Decrypt is deliberately not their mirror image: it leaves <EncryptedData> where it is and returns the decrypted nodes detached, so the caller decides whether to restore them, inspect them, or discard the document. Reinsert with elem.Replace(nodes[0]) for a Type="...#Element" payload.

package examples_test

import (
  "context"
  "crypto/rand"
  "crypto/rsa"
  "fmt"
  "strings"

  "github.com/lestrrat-go/helium"
  "github.com/lestrrat-go/helium/xmlenc1"
)

func Example_xmlenc1_encrypt_decrypt() {
  // Parse a document containing sensitive data. In SAML, this would
  // be an Assertion element inside a Response.
  const src = `<Response><Assertion>sensitive user data</Assertion></Response>`

  doc, err := helium.NewParser().Parse(context.Background(), []byte(src))
  if err != nil {
    fmt.Printf("parse error: %s\n", err)
    return
  }

  // Generate an RSA key pair. In production, use the recipient's
  // public key (e.g., the SP's certificate in SAML).
  key, err := rsa.GenerateKey(rand.Reader, 2048)
  if err != nil {
    fmt.Printf("keygen error: %s\n", err)
    return
  }

  // Encrypt the Assertion element. The Encryptor:
  // 1. Generates a random AES session key
  // 2. Encrypts the serialized element with AES-128-GCM
  // 3. Wraps the session key with RSA-OAEP
  // 4. Replaces the element in the tree with <EncryptedData>
  assertion, ok := helium.AsNode[*helium.Element](doc.DocumentElement().FirstChild())
  if !ok {
    fmt.Println("assertion not found")
    return
  }

  edElem, err := xmlenc1.NewEncryptor().
    BlockAlgorithm(xmlenc1.AES128GCM11).
    KeyTransportAlgorithm(xmlenc1.RSAOAEP).
    RecipientPublicKey(&key.PublicKey).
    EncryptElement(context.Background(), assertion)
  if err != nil {
    fmt.Printf("encrypt error: %s\n", err)
    return
  }

  encrypted, _ := helium.WriteString(doc)
  fmt.Println(strings.Contains(encrypted, "sensitive user data"))
  fmt.Println(strings.Contains(encrypted, "EncryptedData"))

  // Decrypt returns the original node(s). The caller decides whether
  // to re-insert them into the tree or process them standalone.
  nodes, err := xmlenc1.NewDecryptor().PrivateKey(key).
    Decrypt(context.Background(), edElem)
  if err != nil {
    fmt.Printf("decrypt error: %s\n", err)
    return
  }

  decrypted, _ := helium.WriteString(nodes[0])
  fmt.Println(strings.Contains(decrypted, "sensitive user data"))
  // Output:
  // false
  // true
  // true
}

source: examples/xmlenc1_encrypt_decrypt_example_test.go

W3C interop conformance

The sibling helium-w3c-tests module runs the XML Encryption 1.1 core vectors with the xmlenc11 suite. The current conformance summary records all ten vectors as passing, none skipped and none failing.

Those ten vectors exercise key protection: six ECDH-ES ConcatKDF cases on EC-P256, EC-P384, and EC-P521, and four rsa-oaep cases on RSA-2048, RSA-3072, and RSA-4096. Every one of them uses AES-GCM block encryption. So the snapshot is evidence about how the session key is protected and about AES-GCM, and it is evidence about nothing else: no CBC block algorithm appears in it, the Triple DES algorithms the conformance scope above refuses are not covered by it, and it is not the merlin interop corpus. The ten are the suite in full, and the 1.1 interop corpus holds no Triple DES vector, so the zero skips is not a vector being passed over.

No interop vector covers xenc:CipherReference, in this suite or in any other corpus the harness fetches. The one CipherReference vector in the Apache Santuario corpus needs BOTH an XPath transform and aes192-cbc, and this package implements neither, so there is nothing runnable to add and no skip entry to write — a skip would imply a vector is being passed over when none exists. The feature's evidence is this package's own tests.

This package's own tests carry the AES-CBC evidence, against a third-party vector. TestInteropRetrievalMethod decrypts merlin-xmlenc-five/encrypt-element-aes256-cbc-retrieved-kw-aes256.xml from the Apache Santuario corpus end to end, which exercises aes256-cbc block decryption, kw-aes256 unwrapping, a same-document ds:RetrievalMethod, and padding written by another implementation, all on a document nothing here produced.

The suite gates releases: release.yml's conformance-gate matrix runs it against the pinned harness commit, and a release is neither tagged nor published unless it reports zero failures. It is also available on demand through the manual Conformance workflow.

Run it from ../helium-w3c-tests:

go run ./cmd/w3cgen fetch xmlenc11
go run ./cmd/w3ctest xmlenc11

Documentation

Overview

Package xmlenc1 implements W3C XML Encryption 1.1, naming each deliberate departure from the specification and its reason.

Covered: AES block encryption, AES key wrapping, RSA-OAEP key transport, ECDH-ES key agreement with ConcatKDF, same-document ds:RetrievalMethod, and xenc:CipherReference — same-document by default, and external through Decryptor.CipherReferenceResolver.

The one construct xmlenc-core1 marks REQUIRED and this package does not implement is Triple DES: the algorithms #tripledes-cbc and #kw-tripledes are refused deliberately, because Triple DES is a 64-bit block cipher that Sweet32 (CVE-2016-2183) applies to. README.md's Conformance scope section owns the detail.

Index

Constants

View Source
const (
	// NamespaceXMLEnc is the XML Encryption namespace.
	NamespaceXMLEnc = "http://www.w3.org/2001/04/xmlenc#"

	// NamespaceXMLEnc11 is the XML Encryption 1.1 namespace.
	NamespaceXMLEnc11 = "http://www.w3.org/2009/xmlenc11#"

	// NamespaceDSig is the XML Digital Signatures namespace (for KeyInfo).
	NamespaceDSig = "http://www.w3.org/2000/09/xmldsig#"

	// NamespaceDSigMore contains the additional XML Digital Signature
	// algorithm identifiers used by XML Encryption.
	NamespaceDSigMore = "http://www.w3.org/2001/04/xmldsig-more#"

	// NamespaceDSig11 is the XML Digital Signature 1.1 namespace.
	NamespaceDSig11 = "http://www.w3.org/2009/xmldsig11#"
)
View Source
const (
	AES128CBC = NamespaceXMLEnc + "aes128-cbc"
	AES256CBC = NamespaceXMLEnc + "aes256-cbc"
	AES128GCM = NamespaceXMLEnc + "aes128-gcm"
	AES256GCM = NamespaceXMLEnc + "aes256-gcm"

	// AES128GCM11 and related constants are XML Encryption 1.1 GCM
	// identifiers, the only namespace in which a W3C XML Security
	// specification defines AES-GCM (xmlenc-core1 §5.2). They are distinct
	// from the two 2001-namespace GCM identifiers above, which no XML
	// Security specification defines.
	AES128GCM11 = NamespaceXMLEnc11 + "aes128-gcm"
	AES192GCM11 = NamespaceXMLEnc11 + "aes192-gcm"
	AES256GCM11 = NamespaceXMLEnc11 + "aes256-gcm"
)

Block encryption algorithm URIs.

View Source
const (
	RSAOAEP   = NamespaceXMLEnc + "rsa-oaep-mgf1p"
	RSAOAEP11 = NamespaceXMLEnc11 + "rsa-oaep"
)

Key transport algorithm URIs.

View Source
const (
	AES128KeyWrap = NamespaceXMLEnc + "kw-aes128"
	AES192KeyWrap = NamespaceXMLEnc + "kw-aes192"
	AES256KeyWrap = NamespaceXMLEnc + "kw-aes256"
)

Key wrapping algorithm URIs.

View Source
const (
	DigestSHA1 = NamespaceDSig + "sha1"
	// DigestSHA224 is the XMLDSig-more SHA-224 URI.
	DigestSHA224 = NamespaceDSigMore + "sha224"
	DigestSHA256 = NamespaceXMLEnc + "sha256"
	DigestSHA384 = NamespaceXMLEnc + "sha384"
	// DigestSHA384DSigMore is the XMLDSig-more SHA-384 URI.
	DigestSHA384DSigMore = NamespaceDSigMore + "sha384"
	DigestSHA512         = NamespaceXMLEnc + "sha512"
)

Digest algorithm URIs (for RSA-OAEP 1.1).

View Source
const (
	MGFSHA1   = NamespaceXMLEnc11 + "mgf1sha1"
	MGFSHA224 = NamespaceXMLEnc11 + "mgf1sha224"
	MGFSHA256 = NamespaceXMLEnc11 + "mgf1sha256"
	MGFSHA384 = NamespaceXMLEnc11 + "mgf1sha384"
	MGFSHA512 = NamespaceXMLEnc11 + "mgf1sha512"
)

MGF algorithm URIs.

View Source
const (
	ECDHES    = NamespaceXMLEnc11 + "ECDH-ES"
	ConcatKDF = NamespaceXMLEnc11 + "ConcatKDF"
)

Key agreement and key derivation algorithm URIs.

View Source
const (
	TypeElement = NamespaceXMLEnc + "Element"
	TypeContent = NamespaceXMLEnc + "Content"
)

Encryption type URIs.

View Source
const DefaultBlockAlgorithm = AES256GCM11

DefaultBlockAlgorithm is the block encryption algorithm an Encryptor uses when no BlockAlgorithm is set. It is authenticated AES-256-GCM under the XML Encryption 1.1 identifier AES256GCM11, the only namespace in which a W3C XML Security specification defines AES-GCM (xmlenc-core1 §5.2), so the default output is what a conforming peer recognizes.

View Source
const DefaultMaxCipherValueBytes = 10 << 20

DefaultMaxCipherValueBytes bounds the decoded EncryptedData CipherValue payload when Decryptor.MaxCipherValueBytes is not set. It matches helium's default maximum size for an individual XML content node and prevents a payload split across CDATA nodes from bypassing that parser-level limit.

View Source
const DefaultMaxEncryptedKeyBytes = 64 << 10

DefaultMaxEncryptedKeyBytes bounds the total decoded <EncryptedKey> ciphertext of one EncryptedData when Decryptor.MaxEncryptedKeyBytes is not set, which owns what the budget covers and when it is charged.

64 KiB fits DefaultMaxEncryptedKeys recipients at 512 bytes each — an RSA-4096 wrapped key, the largest in ordinary use — and still leaves 14 KiB spare. Real wrapped keys are usually far smaller: 24 to 40 bytes for AES key wrap, 256 bytes for RSA-2048.

View Source
const DefaultMaxEncryptedKeys = 100

DefaultMaxEncryptedKeys bounds how many <EncryptedKey> candidates a Decryptor will trial-decrypt for a single EncryptedData when MaxEncryptedKeys is not set. An unbounded count is a CPU amplification (DoS) vector. Decryptor.MaxEncryptedKeys documents the per-candidate cost this bounds. The default mirrors jwx's WithMaxRecipients (100), which is generous for real multi-recipient documents yet caps amplification.

View Source
const (
	// TypeEncryptedKey is the Type a ds:RetrievalMethod states to link to the
	// xenc:EncryptedKey holding the key needed to decrypt the CipherData of
	// the EncryptedData or EncryptedKey whose ds:KeyInfo carries it. A
	// RetrievalMethod stating it must name an xenc:EncryptedKey; naming
	// anything else is refused as ErrMalformedEncrypted.
	TypeEncryptedKey = NamespaceXMLEnc + "EncryptedKey"
)

Type URIs a ds:RetrievalMethod inside a ds:KeyInfo may declare (xmlenc-core1 §3.5.3).

Variables

View Source
var (
	// ErrDecryptionFailed is returned when decryption fails.
	ErrDecryptionFailed = errors.New("xmlenc1: decryption failed")

	// ErrEncryptionFailed is returned when encryption fails.
	ErrEncryptionFailed = errors.New("xmlenc1: encryption failed")

	// ErrMissingKey is returned when no decryption key is available.
	ErrMissingKey = errors.New("xmlenc1: no decryption key available")

	// ErrUnsupportedKeyDerivation is returned when an EncryptedData offers its
	// session key ONLY through an xenc11:DerivedKey (xmlenc-core1 §3.5.2),
	// which tells the recipient to derive the content key from master key
	// material it already holds. This package implements no key derivation from
	// a master key, so it cannot decrypt such a document.
	//
	// It is deliberately NOT ErrMissingKey. That sentinel says no decryption key
	// is available, which sends a caller to audit the keys it configured — and no
	// key it configures can help, because the document asked for a facility this
	// package does not have. A caller must be able to tell "I set this up wrong"
	// from "helium cannot read this document at all". Match with errors.Is.
	//
	// It covers both ways a ds:KeyInfo offers the construct: an xenc11:DerivedKey
	// carried inline, and a ds:RetrievalMethod whose Type names one. It is raised
	// only when derivation was the ONLY option: an EncryptedData that ALSO
	// carries a usable xenc:EncryptedKey decrypts under that key, since the
	// unimplemented construct costs a document nothing it could otherwise do.
	//
	// A pre-shared [Decryptor.SessionKey] never reaches it. That early return
	// precedes key resolution entirely, so a caller holding the session key
	// decrypts a document whose ds:KeyInfo this package cannot read.
	ErrUnsupportedKeyDerivation = errors.New("xmlenc1: key derivation from master key material is not supported")

	// ErrTooManyEncryptedKeys is returned when an EncryptedData carries more
	// EncryptedKey candidates than the Decryptor's effective limit, which
	// guards against CPU amplification (DoS). Decryptor.MaxEncryptedKeys owns
	// the cap: the per-candidate cost it bounds and the effective-limit
	// rules. See also DefaultMaxEncryptedKeys.
	ErrTooManyEncryptedKeys = errors.New("xmlenc1: too many EncryptedKey candidates")

	// ErrEncryptedKeyBytesExceeded is returned when the EncryptedKey
	// candidates of one EncryptedData carry more ciphertext together than
	// the Decryptor's effective byte budget, which guards against memory
	// amplification (DoS). Decryptor.MaxEncryptedKeyBytes owns the budget:
	// what it covers, when it is charged, and the effective-limit rules.
	// See also DefaultMaxEncryptedKeyBytes.
	//
	// It is distinct from ErrTooManyEncryptedKeys because the two bound
	// different things and are raised or lifted by different setters: too
	// many candidates is a count, too much ciphertext is a size, and a
	// caller handling one must be able to tell which limit to raise.
	ErrEncryptedKeyBytesExceeded = errors.New("xmlenc1: EncryptedKey ciphertext exceeds the byte budget")

	// ErrCipherValueBytesExceeded is returned when an EncryptedData payload
	// CipherValue exceeds the Decryptor's effective byte budget. The payload
	// may arrive as many text or CDATA nodes, so this limit is separate from
	// the parser's per-node content limit. Decryptor.MaxCipherValueBytes owns
	// the budget, including its effective-limit rules. See also
	// DefaultMaxCipherValueBytes.
	ErrCipherValueBytesExceeded = errors.New("xmlenc1: EncryptedData CipherValue exceeds the byte budget")

	// ErrReferenceNotFound is returned when a ds:RetrievalMethod or an
	// xenc:CipherReference names something this package will not resolve: a
	// same-document reference matching no element in the document the
	// EncryptedData belongs to, or a URI that is not a same-document reference
	// at all and cannot be dereferenced. The shipped FSReferenceResolver wraps
	// it for every URI shape it refuses and for a resource it cannot read.
	//
	// The two constructs differ in whether an external URI can ever be
	// resolved:
	//
	//   - a ds:RetrievalMethod naming another resource is refused whatever it
	//     names, with no setting that lifts the refusal. Only the
	//     same-document form is REQUIRED (xmlenc-core1 §3.5), no external form
	//     is mandated anywhere, and an external key location decides which key
	//     material the recipient trial-decrypts — not a decision a document
	//     gets to make for a caller.
	//   - an xenc:CipherReference naming another resource is refused until the
	//     caller supplies a Decryptor.CipherReferenceResolver. §3.3.1 imports
	//     XMLDSig's dereferencing model, which makes the same-document forms
	//     MUSTs and HTTP dereferencing RECOMMENDED, so the external form is an
	//     opt-in capability, and no obligation.
	//
	// The reference is resolved while the document is read, so this precedes
	// the Decryptor.SessionKey early return: a pre-shared session key does not
	// decrypt past a reference that was refused. A ds:RetrievalMethod whose
	// Type this package does not implement is never resolved at all and so
	// never reaches this error. Match with errors.Is.
	ErrReferenceNotFound = errors.New("xmlenc1: reference not found")

	// ErrAmbiguousReference is returned when a same-document
	// ds:RetrievalMethod or xenc:CipherReference URI matches more than one
	// element.
	//
	// This is XML Signature Wrapping applied to encryption. An attacker who
	// can inject an element carrying an Id already in use would otherwise
	// choose which of the two the recipient resolves, and so which key it
	// unwraps and trial-decrypts with, or which octets it takes as the cipher
	// text. Resolution therefore collects every match and refuses on more than
	// one, taking neither. Match with errors.Is.
	ErrAmbiguousReference = errors.New("xmlenc1: ambiguous reference")

	// ErrKeyUnwrapFailed is returned when AES key unwrap integrity check
	// fails. It is always wrapped in ErrDecryptionFailed, so a caller that
	// tests only for ErrDecryptionFailed catches a failed key unwrap the
	// same way it catches a failed RSA key transport.
	ErrKeyUnwrapFailed = errors.New("xmlenc1: AES key unwrap integrity check failed")

	// ErrMalformedEncrypted is returned when an EncryptedData element is malformed.
	ErrMalformedEncrypted = errors.New("xmlenc1: malformed EncryptedData element")

	// ErrOpaquePayload is returned by Decryptor.Decrypt when the
	// EncryptedData's @Type does not declare XML content: it is absent, empty,
	// or a URI other than TypeElement and TypeContent. Decrypt is the XML
	// path, so it refuses such a payload, and every message wrapping this
	// sentinel names Decryptor.DecryptBytes, which returns the plaintext
	// octets without parsing them.
	//
	// @Type sits OUTSIDE the ciphertext and is authenticated by nothing, not
	// even AES-GCM. Treating an absent or unrecognized value as TypeElement
	// would let anyone who can edit the document delete the attribute and have
	// an opaque octet stream parsed as XML and handed back as nodes to graft
	// into a tree — type confusion decided by an attribute the recipient
	// cannot verify. xmlenc-core1 §4.2 asks a decryptor to take an unknown or
	// empty Type as a signal that the cleartext is an opaque octet stream, and
	// §3.1 puts the absent case in the same position.
	//
	// It is deliberately NOT ErrMalformedEncrypted: such a document is
	// well-formed, and a caller must be able to tell "this payload is opaque,
	// retry with DecryptBytes" from a document it should reject outright.
	// DecryptBytes never returns it — it does not read @Type at all. Match
	// with errors.Is.
	ErrOpaquePayload = errors.New("xmlenc1: EncryptedData payload is not XML")

	// ErrMissingConfig is returned when required encryption config is missing.
	ErrMissingConfig = errors.New("xmlenc1: missing required configuration")

	// ErrConflictingKeyConfig is returned when an Encryptor configures two of
	// the three ways to protect the session key: RSA key transport
	// (KeyTransportAlgorithm + RecipientPublicKey), ECDH-ES key agreement
	// (KeyWrapAlgorithm + RecipientECPublicKey), and AES key wrapping
	// (KeyWrapAlgorithm + KeyEncryptionKey). Any pair of them fails with this
	// error, naming both of the configured things so the caller knows which
	// two to choose between. A SessionKey alongside a single mechanism is
	// not a pair: it supplies the key that mechanism protects.
	//
	// An EncryptedData carries a single EncryptedKey here, so honoring one
	// mechanism means silently discarding the other — and a recipient
	// holding only the discarded key then fails to decrypt with an error
	// that points nowhere near the real mistake. The caller must pick one.
	ErrConflictingKeyConfig = errors.New("xmlenc1: conflicting key protection configured")

	// ErrConflictingBlockAlgorithm is returned when an EncryptedData declares a
	// block algorithm in its EncryptionMethod and the Decryptor was given a
	// different one through Decryptor.BlockAlgorithm. The message names both
	// URIs so the caller knows which of the two to change.
	//
	// The two are matched on purpose, and never ordered. Decryptor.BlockAlgorithm
	// exists for an EncryptedData that carries no EncryptionMethod at all, where
	// the algorithm is known out of band (W3C xmlenc-core1 §3.1, §4.4); letting a
	// document's declaration win over a caller who stated the algorithm would let
	// the document choose the cipher the recipient runs, which is exactly the
	// algorithm confusion the setter must not introduce. Under a strict match,
	// setting it can only narrow what a decrypt accepts.
	ErrConflictingBlockAlgorithm = errors.New("xmlenc1: conflicting block algorithm")

	// ErrCBCRequiresOptIn is returned when a Decryptor is asked to
	// decrypt an AES-CBC ciphertext but the caller has not opted in
	// to unauthenticated CBC via Decryptor.AllowUnauthenticatedCBC(true).
	//
	// AES-CBC under XML Encryption 1.0 is unauthenticated and is
	// vulnerable to padding-oracle attacks (Jager/Somorovsky 2011).
	// XML Encryption 1.1 deprecated CBC in favor of AES-GCM. Callers
	// that must interoperate with legacy CBC ciphertexts can opt in
	// after evaluating the attack surface (e.g. ensuring decryption
	// errors are not exposed to remote attackers).
	ErrCBCRequiresOptIn = errors.New("xmlenc1: AES-CBC decryption requires AllowUnauthenticatedCBC(true)")

	// ErrCBCEncryptionRequiresOptIn is returned when an Encryptor is
	// configured to emit a new AES-CBC ciphertext (via a CBC
	// BlockAlgorithm) but the caller has not opted in to legacy CBC
	// encryption via Encryptor.AllowLegacyCBC(true).
	//
	// The Encryptor defaults to AES-256-GCM (authenticated). AES-CBC
	// under XML Encryption 1.0 is unauthenticated and vulnerable to
	// padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption
	// 1.1 deprecated it in favor of AES-GCM. Emitting new CBC
	// ciphertext therefore requires an explicit acknowledgement.
	ErrCBCEncryptionRequiresOptIn = errors.New("xmlenc1: AES-CBC encryption requires AllowLegacyCBC(true)")
)

Functions

This section is empty.

Types

type ConcatKDFParams added in v0.8.0

type ConcatKDFParams struct {
	// AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, and SuppPrivInfo
	// are the NIST SP 800-56A OtherInfo fields, decoded from the hexBinary
	// attributes of the same names. They are concatenated, in this order,
	// into the KDF input, so both parties must agree on them exactly.
	//
	// The five fields TOGETHER are limited to 4096 bytes, since the document
	// under decryption is attacker-supplied and the concatenation costs work
	// proportional to its size. Real OtherInfo is identifiers and nonces —
	// tens of bytes — so the limit is far above any interoperable value.
	// Exceeding it is an error wrapping [ErrMalformedEncrypted], raised when
	// parsing a document and when deriving from these parameters.
	//
	// One parameter set never reaches a derivation and so is never measured:
	// the fallback [Encryptor.KeyDerivationParams] documents replaces a set
	// whose DigestMethod is empty, wholesale, with the SHA-256 default
	// carrying empty OtherInfo. These five fields are discarded there rather
	// than checked, so an oversized set paired with an empty DigestMethod
	// encrypts successfully and emits no OtherInfo attributes at all.
	AlgorithmID  []byte
	PartyUInfo   []byte
	PartyVInfo   []byte
	SuppPubInfo  []byte
	SuppPrivInfo []byte
	// DigestMethod is the hash driving the KDF, taken from the @Algorithm
	// of the ds:DigestMethod child. Parsed wire parameters must carry one:
	// a xenc11:ConcatKDFParams without it is rejected as malformed. On an
	// Encryptor these parameters are configuration, and no wire data,
	// and [Encryptor.KeyDerivationParams] states what an empty DigestMethod
	// means there.
	DigestMethod string
	// contains filtered or unexported fields
}

ConcatKDFParams contains the XML Encryption 1.1 ConcatKDF parameters. The parameter attributes are decoded from their hexBinary representation; their unused-bit counts are retained internally for KDF bit-string packing.

type Decryptor

type Decryptor struct {
	// contains filtered or unexported fields
}

Decryptor decrypts XML EncryptedData elements. It uses clone-on-write semantics.

func NewDecryptor

func NewDecryptor() Decryptor

NewDecryptor creates a new Decryptor.

func (Decryptor) AllowUnauthenticatedCBC

func (d Decryptor) AllowUnauthenticatedCBC(v bool) Decryptor

AllowUnauthenticatedCBC opts the Decryptor in to decrypting AES-CBC ciphertexts. AES-CBC under XML Encryption 1.0 is unauthenticated and vulnerable to padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption 1.1 deprecated CBC in favor of AES-GCM.

By default the Decryptor refuses CBC and returns ErrCBCRequiresOptIn. Set this to true only if you must accept legacy CBC ciphertexts AND you have verified that decryption errors are not exposed to remote attackers (e.g. by surfacing the same generic error for every failure path and never timing-sidechannel distinguishing them).

func (Decryptor) BlockAlgorithm added in v0.8.0

func (d Decryptor) BlockAlgorithm(uri string) Decryptor

BlockAlgorithm supplies the block encryption algorithm URI out of band, for an EncryptedData that carries no EncryptionMethod. W3C xmlenc-core1 §3.1 and §3.2 leave that element optional and state that the recipient must then already know the algorithm, and §4.4 step 1 admits obtaining the algorithm information out of band. Support for such a document is therefore opt-in: without this setter one fails with ErrMalformedEncrypted, because nothing says what to decrypt it with.

An empty URI counts as not set. The match against the document is STRICT, so setting this can only narrow what a decrypt accepts, never widen it:

  • EncryptionMethod absent and this unset: ErrMalformedEncrypted, naming this setter.
  • EncryptionMethod absent and this set: the URI set here is used.
  • EncryptionMethod present and this unset: the document's URI is used.
  • Both present and different: ErrConflictingBlockAlgorithm, naming both.

A document can never override a caller who stated the algorithm out of band; that sentinel's godoc owns why. Whichever of the two the resolution returns is the algorithm every later step is bound to, exactly as a wire-declared one is: the AES-CBC opt-in gate (see Decryptor.AllowUnauthenticatedCBC), the additional authenticated data of the two 2001-namespace GCM identifiers, the session-key length (KeySizeError), and the length a valid AES key-wrap ciphertext must have.

func (Decryptor) CipherReferenceResolver added in v0.8.0

func (d Decryptor) CipherReferenceResolver(r ReferenceResolver) Decryptor

CipherReferenceResolver supplies the octets of an xenc:CipherReference whose URI is NOT one of the four same-document forms, i.e. one naming a resource outside the document being decrypted. ReferenceResolver owns what a resolver is asked for and what the shipped FSReferenceResolver refuses.

Nil is the default, and it is a deny, and no gap: an external URI then fails closed with ErrReferenceNotFound, and no document can lift that by itself. The same-document forms need no I/O and never reach a resolver, so setting one changes nothing about how they resolve — it only adds the external form. That split follows the specification: W3C xmlenc-core1 §3.3.1 imports XMLDSig's dereferencing model, in which the same-document forms are normative MUSTs (xmldsig-core1 §4.4.3.2, §4.4.3.3) while HTTP dereferencing is RECOMMENDED (§4.4.3.1).

A resolved resource is charged against the same budget its CipherData would have been: Decryptor.MaxCipherValueBytes for an EncryptedData payload and Decryptor.MaxEncryptedKeyBytes for an EncryptedKey. A resolver returns a stream and this package reads it, so the bound holds for a resolver a caller writes and not merely for the one shipped here: the read stops one byte past what the budget still allows, it stops when ctx is done, and the stream is closed on every one of those paths. What the package cannot constrain is what happens INSIDE a resolver — one that buffers a whole resource itself, or blocks before returning a stream at all, does so on the caller's own account. ReferenceResolver states that division and why it falls there.

func (Decryptor) Decrypt

func (d Decryptor) Decrypt(ctx context.Context, elem *helium.Element) ([]helium.Node, error)

Decrypt decrypts an EncryptedData element and returns the decrypted nodes.

Unlike Encryptor.EncryptElement and Encryptor.EncryptContent, which splice EncryptedData into the tree, Decrypt does NOT modify the document: elem stays exactly where it is and the returned nodes are detached. Restoring the original document is the caller's decision — call elem.Replace with the single node for a TypeElement payload, or remove elem and insert the nodes at its position for TypeContent.

The plaintext is parsed with DTD loading, external entity resolution, and network access disabled, in the in-scope-namespace context of elem's parent, so prefixes declared only on an ancestor resolve correctly.

Only the two @Type values that declare XML content are parsed: a TypeContent payload yields its children, and a TypeElement payload must yield exactly one element node. Every other @Type — absent, empty, or an unrecognized URI — marks an opaque octet stream and is refused with ErrOpaquePayload, which explains why an unauthenticated attribute never selects the XML path. Use DecryptBytes for such a payload; it returns the plaintext octets without parsing them.

func (Decryptor) DecryptBytes added in v0.8.0

func (d Decryptor) DecryptBytes(ctx context.Context, elem *helium.Element) ([]byte, error)

DecryptBytes decrypts an EncryptedData element and returns its plaintext octets without parsing them as XML. It does not interpret @Type, so use it for opaque or application-defined binary payloads, including values with no Type attribute.

func (Decryptor) ECPrivateKey added in v0.8.0

func (d Decryptor) ECPrivateKey(key *ecdsa.PrivateKey) Decryptor

ECPrivateKey sets the elliptic-curve private key used for XML Encryption 1.1 ECDH-ES key agreement.

func (Decryptor) KeyEncryptionKey

func (d Decryptor) KeyEncryptionKey(kek []byte) Decryptor

KeyEncryptionKey sets the key for AES key unwrapping.

func (Decryptor) MaxCipherValueBytes added in v0.8.0

func (d Decryptor) MaxCipherValueBytes(n int) Decryptor

MaxCipherValueBytes caps the decoded EncryptedData CipherValue payload, in bytes, that decrypting one EncryptedData will hold. This is independent of MaxEncryptedKeyBytes, which bounds only the wrapped session-key candidates.

The budget is charged while the document is read, before the payload CipherValue is assembled or decoded. It measures decoded octets, so XML whitespace and splitting across text or CDATA nodes cannot bypass it.

Zero (the default) uses DefaultMaxCipherValueBytes; a negative value removes the limit. A document over the effective budget fails with ErrCipherValueBytesExceeded before block decryption or plaintext parsing.

func (Decryptor) MaxEncryptedKeyBytes added in v0.8.0

func (d Decryptor) MaxEncryptedKeyBytes(n int) Decryptor

MaxEncryptedKeyBytes caps the total decoded <EncryptedKey> ciphertext, in bytes, that decrypting one EncryptedData will hold. Decryptor.MaxEncryptedKeys bounds how many candidates a document may carry; this bounds how large they may be together, which that count alone does not.

The budget is charged while the document is read, before each retained candidate's CipherValue is assembled or decoded. An excess candidate is rejected by MaxEncryptedKeys before this budget or its structure is read. A CipherValue the base64 decoder would reject is charged what that rejected decode costs, so malformed ciphertext cannot buy work the budget was set to deny. Only <EncryptedKey> ciphertext counts. The EncryptedData payload is charged separately by MaxCipherValueBytes.

What the budget bounds is memory held for a candidate, not the length of the text it was written as. A CipherValue may carry XML whitespace between its characters and may be spread over any number of text and CDATA nodes, none of which changes the bytes it decodes to; that text is counted where it lies and never gathered into a value of its own, so an unbounded amount of it costs nothing beyond reading it.

Zero (the default) uses DefaultMaxEncryptedKeyBytes; a negative value removes the limit (matching helium's MaxDepth convention). A document over the effective budget fails with ErrEncryptedKeyBytesExceeded, in every key configuration: the budget is charged during parsing, so it holds ahead of both the candidate loop and the Decryptor.SessionKey early return.

func (Decryptor) MaxEncryptedKeys added in v0.4.0

func (d Decryptor) MaxEncryptedKeys(n int) Decryptor

MaxEncryptedKeys caps the number of <EncryptedKey> candidates the Decryptor will trial-decrypt for a single EncryptedData. A document packed with junk EncryptedKey elements is a CPU amplification (DoS) vector, so the cap is enforced while parsing before an excess candidate is parsed or retained.

A candidate's branch — which key it uses and what it costs — is dispatched on its AgreementMethod first and on its declared algorithm second. An EncryptedKey carrying an AgreementMethod takes the key-agreement branch and uses Decryptor.ECPrivateKey; its declared algorithm is the AES key-wrap URI applied to the agreed key and does not choose the branch. Only a supported ECDH-ES agreement URI then reaches the full cost of a key agreement, a ConcatKDF derivation, and an AES key unwrap; any other agreement URI is rejected before all three. Without an AgreementMethod the declared algorithm decides: an RSA-OAEP URI uses Decryptor.PrivateKey and costs a private-key decrypt, an AES key-wrap URI uses Decryptor.KeyEncryptionKey and costs a plain key unwrap. A candidate whose branch needs a key the caller never configured costs no crypto at all and yields ErrMissingKey.

Zero (the default) uses DefaultMaxEncryptedKeys; a negative value removes the limit (matching helium's MaxDepth convention). A document exceeding the effective cap fails with ErrTooManyEncryptedKeys, in every key configuration: the cap is applied while parsing the candidate list, before the Decryptor.SessionKey early return.

func (Decryptor) PrivateKey

func (d Decryptor) PrivateKey(key *rsa.PrivateKey) Decryptor

PrivateKey sets the RSA private key for key transport decryption.

PrivateKey, ECPrivateKey, and KeyEncryptionKey may all be set at once, so a single Decryptor handles documents protected different ways. Decryptor.MaxEncryptedKeys states which of the three an EncryptedKey candidate uses. A non-empty Decryptor.SessionKey makes all three inert.

func (Decryptor) SessionKey

func (d Decryptor) SessionKey(key []byte) Decryptor

SessionKey sets a pre-shared session key directly. As on the Encryptor, an empty or nil key counts as not set, and decryption falls back to the EncryptedKey candidates.

A non-empty key is not a preference among keys; it is an early return. Decrypt and DecryptBytes take it as the session key and return before candidate selection, per-candidate validation, and per-candidate key resolution, none of which runs. The Decryptor.MaxEncryptedKeys cap is applied ahead of that return and still holds. Every consequence follows from that one fact:

  • PrivateKey, ECPrivateKey, and KeyEncryptionKey have no effect.
  • An EncryptedKey that only candidate selection would reject — a missing EncryptionMethod, an unsupported algorithm URI, an algorithm whose key the caller never configured — does not fail the decrypt.

Set it only when the session key is known out of band.

func (Decryptor) StrictPKCS7Padding added in v0.8.0

func (d Decryptor) StrictPKCS7Padding(v bool) Decryptor

StrictPKCS7Padding narrows which AES-CBC padding a decrypt accepts from the XML Encryption rule to the PKCS#7 one. It is off by default and it has no effect on AES-GCM, which does not pad.

W3C xmlenc-core1 §5.2.1 pads a plaintext short by N octets with N-1 octets of ARBITRARY value and a final octet N, so the only thing a conforming decryptor may read is that final octet. PKCS#7 (RFC 5652 §6.3) additionally fixes every one of those octets to N. PKCS#7 padding is therefore always valid XML Encryption padding, and the reverse does not hold: a peer that fills the leading octets with anything else — random bytes above all — writes a perfectly conforming ciphertext that this option refuses. Turning it on can only narrow what a decrypt accepts, and what it excludes is conforming documents, so leave it off for interoperability.

It exists for a caller who controls both ends and wants the tighter check on what it accepts back. That check is worth something small and specific: under the XML Encryption rule any final octet from 1 to the block size is acceptable, so roughly one in sixteen random forgeries survives unpadding, while under PKCS#7 roughly one in 2^(8N) does. Both numbers describe an unauthenticated mode. Neither closes the padding oracle, whose signal is the success or failure of the decrypt as a whole, and Decryptor.AllowUnauthenticatedCBC states the rest of that reasoning. A caller who wants the ciphertext authenticated wants AES-GCM instead.

Encryptor always writes PKCS#7-shaped padding, which both rules accept, so this option never rejects a document this package produced.

type Encryptor

type Encryptor struct {
	// contains filtered or unexported fields
}

Encryptor encrypts an XML element, an element's content, or arbitrary octets — one terminal method each. It uses clone-on-write semantics: each builder method returns a new Encryptor and the original is never mutated.

func NewEncryptor

func NewEncryptor() Encryptor

NewEncryptor creates a new Encryptor with default settings.

func (Encryptor) AllowLegacyCBC added in v0.3.0

func (e Encryptor) AllowLegacyCBC(v bool) Encryptor

AllowLegacyCBC opts the Encryptor in to emitting unauthenticated AES-CBC ciphertext when a CBC BlockAlgorithm is selected.

The Encryptor defaults to authenticated AES-GCM. AES-CBC under XML Encryption 1.0 is unauthenticated and vulnerable to padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption 1.1 deprecated it in favor of AES-GCM. Set this to true only when you must produce ciphertext for a legacy recipient that cannot accept AES-GCM. This does not affect decryption (see Decryptor.AllowUnauthenticatedCBC).

func (Encryptor) BlockAlgorithm

func (e Encryptor) BlockAlgorithm(uri string) Encryptor

BlockAlgorithm sets the block encryption algorithm URI. If never set, the Encryptor defaults to DefaultBlockAlgorithm (authenticated AES-256-GCM).

Selecting an AES-CBC algorithm (AES128CBC / AES256CBC) additionally requires AllowLegacyCBC(true): CBC under XML Encryption 1.0 is unauthenticated and padding-oracle-prone, so emitting new CBC ciphertext is gated behind an explicit opt-in. Without it, encryption returns ErrCBCEncryptionRequiresOptIn.

func (Encryptor) EncryptBytes added in v0.8.0

func (e Encryptor) EncryptBytes(ctx context.Context, doc *helium.Document, plaintext []byte) (*helium.Element, error)

EncryptBytes encrypts arbitrary octets and returns a detached EncryptedData element owned by doc. It is the counterpart of Decryptor.DecryptBytes: together they cover the payloads that are not an XML element or element content.

The returned element carries no Type attribute, which is what xmlenc-core1 §3.1 asks of a plaintext that is neither an element nor element content. Recover this payload with DecryptBytes, which returns the plaintext octets without parsing them as XML; Decrypt refuses it with ErrOpaquePayload, parsing no octets whose Type never declared XML. No tree is modified — the caller decides where to insert the element.

func (Encryptor) EncryptContent

func (e Encryptor) EncryptContent(ctx context.Context, elem *helium.Element) (*helium.Element, error)

EncryptContent encrypts the content of an element, replacing the children with an EncryptedData element. Returns the EncryptedData element.

This mutates the document: every child of elem is unlinked and the EncryptedData becomes its only child. elem itself stays in place.

func (Encryptor) EncryptElement

func (e Encryptor) EncryptElement(ctx context.Context, elem *helium.Element) (*helium.Element, error)

EncryptElement encrypts an entire element, replacing it in the tree with an EncryptedData element. Returns the EncryptedData element.

This mutates the document: elem is unlinked from its position and the EncryptedData takes its place among the siblings. Decryptor.Decrypt is not the mirror image — it leaves the tree alone and returns the nodes.

func (Encryptor) KeyDerivationParams added in v0.8.0

func (e Encryptor) KeyDerivationParams(params *ConcatKDFParams) Encryptor

KeyDerivationParams sets the ConcatKDF parameters used by ECDH-ES key agreement. It has no effect without RecipientECPublicKey.

The five OtherInfo fields are concatenated into the KDF input exactly as given, so the recipient must derive with identical values; they travel on the wire in the emitted xenc11:ConcatKDFParams. A nil params, or one with an empty DigestMethod, falls back to SHA-256 with empty OtherInfo. ConcatKDFParams states the size limit the five OtherInfo fields share. An encryption whose params name a DigestMethod and exceed that limit fails, emitting no document a hardened recipient would refuse. Params with an empty DigestMethod take the fallback above instead: their OtherInfo is discarded before any derivation, so it is never measured against the limit and never reaches the wire.

The parameters are copied, byte slices included, so mutating the caller's arrays afterwards cannot change what a later encryption derives or emits.

func (Encryptor) KeyEncryptionKey

func (e Encryptor) KeyEncryptionKey(kek []byte) Encryptor

KeyEncryptionKey sets the key encryption key for AES key wrapping. Together with KeyWrapAlgorithm it selects that mechanism, one of the mechanisms that protect the session key; ErrConflictingKeyConfig states how many of them an Encryptor may configure.

func (Encryptor) KeyTransportAlgorithm

func (e Encryptor) KeyTransportAlgorithm(uri string) Encryptor

KeyTransportAlgorithm sets the key transport algorithm URI. Together with RecipientPublicKey it selects RSA key transport, one of the mechanisms that protect the session key; ErrConflictingKeyConfig states how many of them an Encryptor may configure.

func (Encryptor) KeyWrapAlgorithm

func (e Encryptor) KeyWrapAlgorithm(uri string) Encryptor

KeyWrapAlgorithm sets the AES Key Wrap algorithm URI. It names the wrap itself, not which mechanism performs it: with KeyEncryptionKey it selects AES key wrapping under the supplied key, and with RecipientECPublicKey it is the wrap applied to the key ECDH-ES derives. Those are two different mechanisms, and ErrConflictingKeyConfig states how many of them an Encryptor may configure.

func (Encryptor) OAEPDigest

func (e Encryptor) OAEPDigest(uri string) Encryptor

OAEPDigest sets the digest algorithm for RSA-OAEP 1.1.

func (Encryptor) OAEPMGF

func (e Encryptor) OAEPMGF(uri string) Encryptor

OAEPMGF sets the MGF algorithm for RSA-OAEP 1.1.

func (Encryptor) OAEPParams

func (e Encryptor) OAEPParams(params []byte) Encryptor

OAEPParams sets the RSA-OAEP label. A label over the 1 KiB limit [encryptionMethod.OAEPParams] documents fails the encryption with ErrEncryptionFailed, and only when key transport is the mechanism in use, since that is the only one that writes a label.

func (Encryptor) RecipientECPublicKey added in v0.8.0

func (e Encryptor) RecipientECPublicKey(key *ecdsa.PublicKey) Encryptor

RecipientECPublicKey sets the recipient's elliptic-curve public key and selects XML Encryption 1.1 ECDH-ES key agreement. It is the encrypt-side counterpart of Decryptor.ECPrivateKey, and supports P-256, P-384, and P-521.

ECDH-ES derives the key-encryption key, and takes none, so KeyWrapAlgorithm still selects the AES Key Wrap variant applied to the session key, while KeyEncryptionKey belongs to the separate AES key wrapping mechanism; ErrConflictingKeyConfig states how many mechanisms an Encryptor may configure. Each encryption generates a fresh ephemeral key pair, and the EncryptedKey carries its public half in an xenc:AgreementMethod.

The key derivation is ConcatKDF; Encryptor.KeyDerivationParams controls it.

func (Encryptor) RecipientPublicKey

func (e Encryptor) RecipientPublicKey(key *rsa.PublicKey) Encryptor

RecipientPublicKey sets the recipient's RSA public key for key transport.

func (Encryptor) SessionKey

func (e Encryptor) SessionKey(key []byte) Encryptor

SessionKey sets a pre-existing session key. An empty or nil key counts as not set: a random key of the length the block algorithm requires is generated per encryption, and that is also what an Encryptor that never calls SessionKey does.

The key is still protected by whichever mechanism is configured (key transport or key wrapping); supplying it does not skip that step. A non-empty key's length must match the block algorithm exactly, or encryption fails with a KeySizeError, and never silently encrypts at a weaker strength than the emitted @Algorithm claims. An empty or nil key never reaches that check, because the generated key is used instead.

A non-empty key with no protection mechanism configured emits no EncryptedKey, and the recipient must already hold this key. An empty or nil key with no protection mechanism configured leaves nothing configured at all, so encryption fails with ErrMissingConfig.

type KeySizeError added in v0.2.0

type KeySizeError struct {

	// Key names the key that was the wrong length, e.g. "session key" or
	// "key-encryption key". It is diagnostic text and may be empty when
	// the role is not known at the point of failure.
	Key       string
	Algorithm string
	Want      int
	Got       int
	// contains filtered or unexported fields
}

KeySizeError is returned when a key (session key or key-encryption key) does not match the exact length required by its declared algorithm URI. It guards against algorithm/key-size confusion, e.g. declaring AES-256 on the wire while supplying a 16-byte key that crypto/aes would silently treat as AES-128.

Construct it with keyed fields, as in &KeySizeError{Algorithm: uri, Want: want, Got: got}; see UnsupportedAlgorithmError for why an unkeyed literal cannot compile here.

func (*KeySizeError) Error added in v0.2.0

func (e *KeySizeError) Error() string

type ReferenceResolver added in v0.8.0

type ReferenceResolver interface {
	ResolveReference(ctx context.Context, uri string) (io.ReadCloser, error)
}

ReferenceResolver supplies the octet stream an xenc:CipherReference names when its URI is NOT one of the four same-document forms (Decryptor.Decrypt states those forms). It is the opt-in seam for decrypting an EncryptedData whose cipher text lives outside the document carrying it.

A resolver is consulted ONLY for a non-same-document (external) URI. Same-document references need no I/O and never reach it. When no resolver is configured an external reference stays fail-closed with ErrReferenceNotFound, which is the default.

That default is what the specification permits, and no shortfall of it. W3C xmlenc-core1 §3.3.1 requires "the same URI encoding, dereferencing, scheme, and HTTP response codes as that of [XMLDSIG-CORE1]" and defines no dereferencing of its own; xmldsig-core1 §4.4.3.1 makes dereferencing URIs in the HTTP scheme RECOMMENDED, while §4.4.3.2 and §4.4.3.3 make the null URI, the shortname XPointer, and same-document dereferencing normative MUSTs. So the imported obligation covers URI="" and URI="#id", which this package implements unconditionally, and external fetching is a RECOMMENDED capability the caller opts into.

The address space

uri is ALWAYS the JOINED, DOCUMENT-SPACE URI: the @URI the document wrote, resolved against the base URI in force at the xenc:CipherReference element itself (the document's own URL, narrowed by every xml:base on the way down to that element). A resolver therefore never sees the raw attribute and never performs the join. A document parsed with helium.Parser.ParseFile carries the file's absolute path as its URL, so a relative @URI arrives here as an absolute path in that document's space — which is why a filesystem resolver has to be told which prefix of that space its fs.FS stands for. See FSReferenceResolver.

Who bounds and who cancels

The returned stream is the resource's raw bytes: an external reference yields an octet stream, so no canonicalization applies to it and only a declared ds:Transform changes what the bytes mean.

This package reads that stream itself, under the decrypt's remaining CipherValue allowance (Decryptor.MaxCipherValueBytes for a payload reference, Decryptor.MaxEncryptedKeyBytes for a key reference) and under the caller's context. It reads one byte past what the allowance still admits and no further, so it never materializes an oversized resource; it abandons the read the moment ctx is done, and it CLOSES the stream on every path — completion, over-budget, and cancellation alike, and equally when ResolveReference itself returns a stream alongside an error. A resolver hands over a stream and owes it nothing more.

Be precise about what that does and does not buy. It removes THIS PACKAGE's complicity: whatever a URI names, the package holds no more than the budget admits and its own reading stops when the caller says stop. It does NOT stop a resolver from buffering a whole resource internally, or from blocking inside ResolveReference before it returns a stream at all — nothing outside a resolver can constrain what happens inside it. That division is the right one because the two sides are trusted differently: the resolver is code the caller chose, while the URI is chosen by an unauthenticated document, and it is the URI the package must be immune to.

ResolveReference must be safe to call from multiple goroutines. Returning a nil stream with a nil error is refused as ErrReferenceNotFound, and never dereferenced.

func FSReferenceResolver added in v0.8.0

func FSReferenceResolver(fsys fs.FS, root string) ReferenceResolver

FSReferenceResolver returns a ReferenceResolver that serves external CipherReference URIs from fsys. It performs NO network access.

root declares the document-space prefix fsys stands for, which is what lets the resolver map the joined URI it is handed (ReferenceResolver states that the URI is always the joined one) onto a path fsys knows. A URI lying under root is served as the remainder of the path below it; every other URI is taken as a plain relative path in fsys. An empty root serves relative URIs only, which is what a document parsed from memory produces.

So for a document read with helium.Parser.ParseFile from /srv/docs/index.xml, whose sibling cipher text the document names as URI="ct.bin", the joined URI is /srv/docs/ct.bin, and FSReferenceResolver(os.DirFS("/srv/docs"), "/srv/docs") serves it as "ct.bin".

Whatever root admits, the resolved path is still fail-closed on anything that is not a plain in-tree path:

  • a URI carrying a scheme (http:, https:, file:, urn:, or any "scheme:" per RFC 3986, including a Windows drive letter) is refused — the resolver never interprets a scheme, so it cannot be steered into a fetch;
  • a path escaping the root (an absolute path outside the declared root, or one with ".." segments that leave the root after cleaning) is refused via an fs.ValidPath containment check;
  • a leftover fragment ("#...") is refused.

Every rejection wraps ErrReferenceNotFound, so a caller matches them all with errors.Is. This resolver opens the file and hands the stream over; the decrypt's own allowance bounds how much of it is read, and the package closes it — ReferenceResolver owns that division.

Pass helium.PermissiveFS or an os.Root FS to widen what a caller is willing to serve.

type UnsupportedAlgorithmError

type UnsupportedAlgorithmError struct {

	// Parameter names the algorithm slot that rejected the URI, e.g.
	// "block algorithm" or "MGF algorithm". It is diagnostic text and may
	// be empty when the slot is not known at the point of failure.
	Parameter string
	Algorithm string
	// contains filtered or unexported fields
}

UnsupportedAlgorithmError is returned for unrecognized algorithm URIs.

Construct it with keyed fields, as in &UnsupportedAlgorithmError{Algorithm: uri}. An unexported field makes an unkeyed composite literal fail to compile from another package, which is deliberate and is what lets the field set GROW as the diagnostics improve without breaking a caller: nobody can have written the positional form that a new field would invalidate. Reading the exported fields, comparing values, and recovering the type with errors.As are all unaffected.

func (*UnsupportedAlgorithmError) Error

func (e *UnsupportedAlgorithmError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL