Repository navigation
feat(cms): add a hand-written CMS package for SCEP and RFC 3161 - #285
Merged
Merged
Conversation
Bugs5382
marked this pull request as ready for review
September 30, 2026 06:14
This was referenced Sep 30, 2026
SCEP and RFC 3161 both need more CMS than the certs-only envelope EST writes: signed and verified SignedData with signed attributes, and EnvelopedData with RSA key transport and AES-CBC. internal/cms provides exactly that on encoding/asn1 and the stdlib crypto packages, taking a crypto.Signer or crypto.Decrypter so keys can stay in the TPM. - SignedData: parse and verify (content-type and message-digest checks, signers by issuer and serial or by SKI), build with signed attributes, and the degenerate certs-and-CRLs form. - EnvelopedData: parse, decrypt and build; RSA PKCS#1 v1.5 key transport, AES-128/256-CBC, one uniform decryption error and a constant-time padding check. - Content types id-data, id-signedData, id-envelopedData, id-ct-TSTInfo. - BER input (indefinite lengths, chunked OCTET STRINGs) is normalized to DER before parsing; DER passes through unchanged. - SHA-1, MD5, DES and 3DES are refused. Tests cover RFC 5652 structure, known answers, negative cases and round trips in both directions against OpenSSL 3 (cms and smime). The OpenSSL tests skip locally without OpenSSL 3 and fail under CI. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
The degenerate SignedData encoder moves into internal/cms, shared with SCEP and RFC 3161. CertsOnlyPKCS7 and ParseCertsOnlyPKCS7 keep their signatures and behaviour, and the output is byte-for-byte what it was. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382
force-pushed
the
feat/185-cms-primitives
branch
from
September 30, 2026 10:05
2327802 to
dda9138
Compare
Contributor
Author
|
Closing summary: adds internal/cms, a hand-written RFC 5652 subset (SignedData build and verify, degenerate certs-only, EnvelopedData with RSA key transport and AES-CBC) on the stdlib crypto packages, and moves EST's certs-only writer onto it with byte-identical output. Rebased onto main; CI green on dda9138. Squash-merging with the owner's approval. #185 stays open: the SCEP handler (slices 3 and 4) continues in #288, now retargeted to main. |
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
SCEP needs far more CMS than the certs-only envelope EST writes, and RFC 3161 time-stamp tokens need the same SignedData. This adds
internal/cms, a hand-written CMS subset (RFC 5652) built onencoding/asn1and the stdlib crypto packages. There is no third-party CMS or PKCS#7 library, and the package never holds a key: callers pass acrypto.Signerorcrypto.Decrypter, so a key can stay in the TPM.What it covers:
Verifyreturns each signer's certificate. Path validation, key usage and revocation stay the caller's policy.Signwrites signed attributes (content-type, message-digest, plus caller attributes such as the SCEP authenticated attributes), DER-sorted as RFC 5652 section 5.4 requires.Degeneratewrites the certs-only and CRL-carrying message (SCEP GetCACert and GetCRL, EST /cacerts).EncryptandParseEnvelopedData/Decrypt, with RSA PKCS#1 v1.5 key transport and AES-128-CBC or AES-256-CBC.Decryptasks the key for a PKCS#1 v1.5 decryption withSessionKeyLenset, so a software key answers a bad padding with a random key instead of an error. Every failure after the recipient is found returns the sameErrDecrypt, and the CBC padding check is constant-time.ErrNoRecipientis separate so an RA rollover can try the next certificate.ErrUnsupportedAlgorithm, on both the build and the verify side.openssl cms -stream, and several SCEP clients) send indefinite lengths and chunked OCTET STRINGs. Every parse normalizes them to DER first. DER input passes through byte-for-byte, so the signature over the signed attributes still verifies.EST's certs-only writer moves into the package.
est.CertsOnlyPKCS7andest.ParseCertsOnlyPKCS7keep their signatures and now callinternal/cms. The output is byte-for-byte what it was (checked against the old encoder), and EST's tests are unchanged and pass.This slice has only the primitives. It adds no SCEP or TSA handler and no config.
Design calls:
internal/cms. It is a set of pure functions under protocol handlers that already log each request. Its errors carry structure only, never content, plaintext or key material, so the handlers can log them safely.CRYPTOS_TEST_OPENSSLnames the binary) and fail under CI, so they can't silently stop running.Signer.Attributes, next to the code that picks the TSA certificate.Refs #185
No docs change: an internal package with no user-facing behaviour, config or API.
Verification
internal/cms, and theinternal/estdoc comment now points to itHow this was verified
Tests came first and failed until the package existed. New tests in
internal/cms:crypto.Decrypterorcrypto.Signerinterface.cms -signwith RSA, ECDSA,-keyid,-streamBER and detached, plussmime -sign) verify here, and fail once tampered. An OpenSSL SHA-1 message is refused.openssl cms -verifyandsmime -verify, including a self-signed SCEP-style signer and a TSTInfo message thatcms -cmsout -printshows asid-smime-ct-TSTInfo. OpenSSL rejects our messages once tampered.cms -encryptwith AES-128/256,-keyid,-stream, andsmime -encrypt) decrypt here, and a stranger getsErrNoRecipient.openssl cms -decryptandsmime -decrypt, and OpenSSL refuses the wrong recipient.openssl pkcs7 -print_certsreads our degenerate message, certificates and CRL.gofmt,go vet ./...,golangci-lint run ./...(0 issues),go test ./...(the full suite),task buildand the golic license check pass locally. The internal/cms tests also passed-count=20and-race. The pre-push hook passed.Open PRs #271, #277, #279, #280, #282 and #283 touch none of these files. #271 changes
go.mod, and this PR does not.Before merging: add a closing comment summarizing what was actually done in this PR
(not just the checked boxes).