mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:51:30 +00:00
a546a1bbef
SCEP RFC 8894 + Intune master bundle — Phase 2 of 14.
Implements the new RFC 8894 PKIMessage parse path: EnvelopedData parser
+ decryptor, signerInfo parser + signature verifier, handler dispatch
that tries the RFC 8894 path FIRST and falls through to the legacy MVP
raw-CSR path on any parse failure. Backward compat with lightweight SCEP
clients is preserved by design — no behavior change for any existing
deploy that doesn't set CERTCTL_SCEP_RA_*.
internal/pkcs7/envelopeddata.go (new, ~330 LoC)
* ParseEnvelopedData: parses CMS EnvelopedData per RFC 5652 §6.1, with
optional outer ContentInfo unwrapping. Handles SET OF RecipientInfo
+ IssuerAndSerial form rid (RFC 8894 §3.2.2).
* EnvelopedData.Decrypt: RSA PKCS#1 v1.5 key-trans + AES-CBC (128/192/
256) or DES-EDE3-CBC content decryption with **constant-time PKCS#7
padding strip** (no branch on padding-byte values; closes the
padding-oracle leak surface). Recipient mismatch is BadMessageCheck
per RFC 8894 §3.3.2.2 (NOT BadCertID); every failure mode returns
the same ErrEnvelopedDataDecrypt sentinel to close timing-leak legs
of Bleichenbacher attacks.
* Equivalent to micromdm/scep's cryptoutil/cryptoutil.go::DecryptPKCS-
Envelope (cited in code comments; not vendored — fuzz-target
ownership stays in this sub-package per the operating rule).
internal/pkcs7/signedinfo.go (new, ~370 LoC)
* ParseSignedData / ParseSignerInfos: parses CMS SignedData per RFC
5652 §5.3. Resolves each SignerInfo's SID (IssuerAndSerial v1 OR
[0] SubjectKeyId v3) against the SignedData certificates SET to
pluck the device's transient signing cert.
* SignerInfo.VerifySignature: re-serialises signedAttrs as the
canonical SET OF Attribute (the RFC 5652 §5.4 quirk every CMS
implementation hits — wire form is [0] IMPLICIT but the signature
is over EXPLICIT SET OF). Hashes with SHA-1/SHA-256/SHA-512 +
verifies via RSA PKCS1v15 or ECDSA per the cert's pubkey type.
* Auth-attr extractors: GetMessageType (PrintableString-decimal),
GetTransactionID, GetSenderNonce, GetMessageDigest. SCEP attr OIDs
pinned (RFC 8894 §3.2.1.4).
internal/pkcs7/{envelopeddata,signedinfo}_fuzz_test.go (new)
* FuzzParseEnvelopedData / FuzzParseSignedData / FuzzParseSignerInfos
/ FuzzVerifySignerInfoSignature — every parser certctl adds gets a
panic-safety fuzzer (the fuzz-target-ownership rule from
cowork/CLAUDE.md::Operating Rules). Local 5s runs hit ~270k
executions per parser without panic. Errors are expected for
arbitrary inputs; only panics are bugs.
internal/pkcs7/{envelopeddata,signedinfo}_test.go (new)
* Round-trip tests that materialise real RSA/ECDSA pairs, hand-build
the wire bytes, parse + decrypt + verify, and assert plaintext /
auth-attr equality. The build helpers use this package's ASN1Wrap
primitives directly (asn1.Marshal of structs containing nested
asn1.RawValue is finicky for mixed Class/Tag); gives byte-level
control matching what real SCEP clients emit.
* Negative tests: tampered ciphertext / tampered auth-attrs / wrong
RA / wrong key / mismatched recipients / random garbage all return
the appropriate sentinel error without panic.
internal/service/scep.go
* PKCSReqWithEnvelope: RFC 8894 envelope-aware variant. Returns
*SCEPResponseEnvelope (not error + *SCEPEnrollResult) because RFC
8894 §3.3 mandates a CertRep PKIMessage on every response, even
failures — the handler shouldn't translate Go errors into SCEP
failInfo codes. Returns nil to signal 'invalid challenge password'
so the caller can translate to HTTP 403 (matches MVP path's wire
shape; RFC 8894 §3.3.1 is silent on this case).
* mapServiceErrorToFailInfo: exact mapping table from the prompt
(CSR parse → BadRequest, CSR sig → BadMessageCheck, crypto policy
→ BadAlg, default → BadRequest).
internal/api/handler/scep.go
* SCEPService interface gains PKCSReqWithEnvelope.
* SCEPHandler now optionally carries an RA cert + key pair. SetRAPair
upgrades the handler to the RFC 8894 path; without that call the
handler stays MVP-only (the v2.0.x behavior).
* pkiOperation: tries the RFC 8894 path FIRST when the RA pair is
set. tryParseRFC8894 helper does the full pipeline (ParseSignedData
→ VerifySignature → extract auth-attrs → ParseEnvelopedData → Decrypt
→ x509.ParseCertificateRequest the recovered bytes). On any failure
it falls through to the legacy extractCSRFromPKCS7 MVP path —
backward compat is non-negotiable.
* Phase 2 emits the legacy certs-only response on RFC 8894 success;
Phase 3 (next commit) swaps in writeCertRepPKIMessage with the
proper status / failInfo / nonce-echo wire shape.
cmd/server/main.go
* Per-profile loop now calls loadSCEPRAPair after preflight to load
the cert + key + inject via SetRAPair. crypto + crypto/tls imports
added.
* loadSCEPRAPair helper: tls.X509KeyPair-based parse + leaf cert
extraction. Failures here indicate TOCTOU between preflight + load.
internal/api/handler/scep_handler_test.go +
internal/api/router/router_scep_profiles_test.go
* mockSCEPService / scepProfileMockService gain PKCSReqWithEnvelope
stubs to satisfy the extended interface. Existing test cases
unchanged (they exercise the MVP path; RA pair is unset).
Verification:
* gofmt + go vet clean for the files I touched.
* go test -short -count=1 green across pkcs7 / api/handler /
api/router / service / cmd/server.
* Coverage: pkcs7 78.4% (was 100% — drops because new code includes
paths the round-trip tests don't yet hit, like decryption alg
fall-through and v3 SubjectKeyId SID matching).
* Fuzz-target seed-corpus runs (5s each, ~270k execs/parser): no
panic. Pre-merge fuzz-time bumps to 30s per the prompt's
verification gate.
Phase 2 of 14 in SCEP RFC 8894 + Intune master bundle.
Living progress at cowork/scep-rfc8894-intune/progress.md.
361 lines
13 KiB
Go
361 lines
13 KiB
Go
package pkcs7
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// SCEP RFC 8894 Phase 2.2: round-trip tests for ParseSignedData +
|
|
// SignerInfo.VerifySignature + auth-attr extractors.
|
|
//
|
|
// Each test materialises a real signing cert + signs auth-attrs over a
|
|
// known content, then re-parses and verifies. Catches drift between the
|
|
// signing-side encoding and the verification-side re-serialisation
|
|
// (RFC 5652 §5.4 SET OF Attribute quirk).
|
|
|
|
func TestSignerInfo_RoundTrip_RSAWithSHA256(t *testing.T) {
|
|
signer, signerCert := genTestRSASigner(t)
|
|
signedData := buildTestSignedData(t, signer, signerCert,
|
|
domain.SCEPMessageTypePKCSReq, "txn-12345", []byte("0123456789abcdef"),
|
|
[]byte("encapsulated content (typically EnvelopedData bytes)"))
|
|
|
|
parsed, err := ParseSignedData(signedData)
|
|
if err != nil {
|
|
t.Fatalf("ParseSignedData: %v", err)
|
|
}
|
|
if len(parsed.SignerInfos) != 1 {
|
|
t.Fatalf("len(SignerInfos) = %d, want 1", len(parsed.SignerInfos))
|
|
}
|
|
|
|
si := parsed.SignerInfos[0]
|
|
if err := si.VerifySignature(); err != nil {
|
|
t.Fatalf("VerifySignature: %v", err)
|
|
}
|
|
|
|
// Auth-attr extractors.
|
|
mt, err := si.GetMessageType()
|
|
if err != nil {
|
|
t.Fatalf("GetMessageType: %v", err)
|
|
}
|
|
if mt != domain.SCEPMessageTypePKCSReq {
|
|
t.Errorf("GetMessageType = %d, want %d", mt, domain.SCEPMessageTypePKCSReq)
|
|
}
|
|
tid, err := si.GetTransactionID()
|
|
if err != nil {
|
|
t.Fatalf("GetTransactionID: %v", err)
|
|
}
|
|
if tid != "txn-12345" {
|
|
t.Errorf("GetTransactionID = %q, want %q", tid, "txn-12345")
|
|
}
|
|
nonce, err := si.GetSenderNonce()
|
|
if err != nil {
|
|
t.Fatalf("GetSenderNonce: %v", err)
|
|
}
|
|
if string(nonce) != "0123456789abcdef" {
|
|
t.Errorf("GetSenderNonce = %q, want %q", nonce, "0123456789abcdef")
|
|
}
|
|
}
|
|
|
|
func TestSignerInfo_RoundTrip_ECDSAWithSHA256(t *testing.T) {
|
|
signer, signerCert := genTestECDSASigner(t)
|
|
signedData := buildTestSignedData(t, signer, signerCert,
|
|
domain.SCEPMessageTypeRenewalReq, "txn-ec-1", []byte("nonce-ec-aaaa-bbbb"),
|
|
[]byte("encap content"))
|
|
|
|
parsed, err := ParseSignedData(signedData)
|
|
if err != nil {
|
|
t.Fatalf("ParseSignedData: %v", err)
|
|
}
|
|
si := parsed.SignerInfos[0]
|
|
if err := si.VerifySignature(); err != nil {
|
|
t.Fatalf("VerifySignature (ECDSA): %v", err)
|
|
}
|
|
mt, err := si.GetMessageType()
|
|
if err != nil {
|
|
t.Fatalf("GetMessageType: %v", err)
|
|
}
|
|
if mt != domain.SCEPMessageTypeRenewalReq {
|
|
t.Errorf("GetMessageType = %d, want RenewalReq (17)", mt)
|
|
}
|
|
}
|
|
|
|
func TestSignerInfo_VerifySignature_TamperedAttrs_Refuses(t *testing.T) {
|
|
signer, signerCert := genTestRSASigner(t)
|
|
signedData := buildTestSignedData(t, signer, signerCert,
|
|
domain.SCEPMessageTypePKCSReq, "txn-tamper", []byte("nonce-aaaa-bbbb"),
|
|
[]byte("content"))
|
|
|
|
parsed, err := ParseSignedData(signedData)
|
|
if err != nil {
|
|
t.Fatalf("ParseSignedData: %v", err)
|
|
}
|
|
si := parsed.SignerInfos[0]
|
|
// Tamper with rawSignedAttrs by flipping the last byte. Re-verification
|
|
// must reject — proves the signature is bound to the auth-attr bytes.
|
|
si.rawSignedAttrs[len(si.rawSignedAttrs)-1] ^= 0x01
|
|
if err := si.VerifySignature(); !errors.Is(err, ErrSignerInfoVerify) {
|
|
t.Errorf("VerifySignature(tampered attrs) = %v, want ErrSignerInfoVerify", err)
|
|
}
|
|
}
|
|
|
|
func TestParseSignedData_Empty_Refuses(t *testing.T) {
|
|
if _, err := ParseSignedData(nil); err == nil {
|
|
t.Error("ParseSignedData(nil) = nil, want error")
|
|
}
|
|
if _, err := ParseSignedData([]byte{}); err == nil {
|
|
t.Error("ParseSignedData(empty) = nil, want error")
|
|
}
|
|
}
|
|
|
|
func TestParseSignedData_Garbage_Refuses(t *testing.T) {
|
|
garbage := []byte{0x30, 0x82, 0x05, 0x01, 0x02, 0x03}
|
|
if _, err := ParseSignedData(garbage); err == nil {
|
|
t.Error("ParseSignedData(garbage) = nil, want error")
|
|
}
|
|
}
|
|
|
|
// --- helpers -------------------------------------------------------------
|
|
|
|
type testSigner interface {
|
|
Sign(data []byte) ([]byte, error)
|
|
DigestOID() asn1.ObjectIdentifier
|
|
SignatureOID() asn1.ObjectIdentifier
|
|
}
|
|
|
|
type rsaTestSigner struct{ k *rsa.PrivateKey }
|
|
|
|
func (s *rsaTestSigner) Sign(data []byte) ([]byte, error) {
|
|
h := sha256.Sum256(data)
|
|
return rsa.SignPKCS1v15(rand.Reader, s.k, 0+5, h[:]) // 5 == crypto.SHA256 in crypto.Hash enum
|
|
}
|
|
func (s *rsaTestSigner) DigestOID() asn1.ObjectIdentifier { return OIDSHA256 }
|
|
func (s *rsaTestSigner) SignatureOID() asn1.ObjectIdentifier { return OIDRSAWithSHA256 }
|
|
|
|
type ecdsaTestSigner struct{ k *ecdsa.PrivateKey }
|
|
|
|
func (s *ecdsaTestSigner) Sign(data []byte) ([]byte, error) {
|
|
h := sha256.Sum256(data)
|
|
return ecdsa.SignASN1(rand.Reader, s.k, h[:])
|
|
}
|
|
func (s *ecdsaTestSigner) DigestOID() asn1.ObjectIdentifier { return OIDSHA256 }
|
|
func (s *ecdsaTestSigner) SignatureOID() asn1.ObjectIdentifier { return OIDECDSAWithSHA256 }
|
|
|
|
func genTestRSASigner(t *testing.T) (testSigner, *x509.Certificate) {
|
|
t.Helper()
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("rsa.GenerateKey: %v", err)
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(time.Now().UnixNano() ^ 0xDEAD),
|
|
Subject: pkix.Name{CommonName: "device-rsa"},
|
|
Issuer: pkix.Name{CommonName: "device-rsa"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatalf("CreateCertificate: %v", err)
|
|
}
|
|
cert, err := x509.ParseCertificate(der)
|
|
if err != nil {
|
|
t.Fatalf("ParseCertificate: %v", err)
|
|
}
|
|
return &rsaTestSigner{k: key}, cert
|
|
}
|
|
|
|
func genTestECDSASigner(t *testing.T) (testSigner, *x509.Certificate) {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("ecdsa.GenerateKey: %v", err)
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(time.Now().UnixNano() ^ 0xBEEF),
|
|
Subject: pkix.Name{CommonName: "device-ec"},
|
|
Issuer: pkix.Name{CommonName: "device-ec"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatalf("CreateCertificate: %v", err)
|
|
}
|
|
cert, err := x509.ParseCertificate(der)
|
|
if err != nil {
|
|
t.Fatalf("ParseCertificate: %v", err)
|
|
}
|
|
return &ecdsaTestSigner{k: key}, cert
|
|
}
|
|
|
|
// buildTestSignedData hand-constructs a CMS SignedData with one SignerInfo
|
|
// carrying SCEP authenticated attributes (messageType, transactionID,
|
|
// senderNonce, plus the standard CMS contentType + messageDigest).
|
|
//
|
|
// The signing pipeline mirrors what micromdm/scep + the ChromeOS SCEP
|
|
// client emit: the device hashes the encap content into messageDigest,
|
|
// the auth-attrs are SET-OF re-serialised, hashed, and signed.
|
|
//
|
|
// Implementation note: built directly with ASN1Wrap helpers rather than
|
|
// relying on asn1.Marshal of structs containing asn1.RawValue fields —
|
|
// asn1.Marshal of nested RawValues with mixed Class/Tag has been finicky
|
|
// and the helpers give us byte-level control that matches what's on the wire.
|
|
func buildTestSignedData(t *testing.T, signer testSigner, signerCert *x509.Certificate, messageType domain.SCEPMessageType, transactionID string, senderNonce, encapContent []byte) []byte {
|
|
t.Helper()
|
|
|
|
// 1. messageDigest auth-attr: SHA-256 of the encap content.
|
|
contentDigest := sha256.Sum256(encapContent)
|
|
|
|
// 2. Build each auth-attr as Attribute ::= SEQUENCE { OID, SET OF Value }
|
|
// using the helpers. Marshal each value individually then wrap.
|
|
attrSetBody := buildSCEPAuthAttrs(t, contentDigest[:], messageType, transactionID, senderNonce)
|
|
|
|
// 3. Compute the signature over SET OF Attribute.
|
|
signedAttrsForSig := ASN1Wrap(0x31, attrSetBody)
|
|
sig, err := signer.Sign(signedAttrsForSig)
|
|
if err != nil {
|
|
t.Fatalf("signer.Sign: %v", err)
|
|
}
|
|
|
|
// 4. Build the SignerInfo SEQUENCE byte-by-byte.
|
|
versionBytes := []byte{0x02, 0x01, 0x01} // INTEGER 1
|
|
// SID is IssuerAndSerialNumber: SEQUENCE { Issuer (RDN), SerialNumber INTEGER }
|
|
serialDER, err := asn1.Marshal(signerCert.SerialNumber)
|
|
if err != nil {
|
|
t.Fatalf("marshal serial: %v", err)
|
|
}
|
|
sidBody := append([]byte{}, signerCert.RawIssuer...) // already in DER
|
|
sidBody = append(sidBody, serialDER...)
|
|
sidBytes := ASN1Wrap(0x30, sidBody)
|
|
|
|
// DigestAlgorithm: AlgorithmIdentifier — encode via stdlib (small struct, no nested RawValue issues).
|
|
digestAlgBytes := mustMarshal(t, pkix.AlgorithmIdentifier{Algorithm: signer.DigestOID(), Parameters: asn1.NullRawValue})
|
|
|
|
// SignedAttrs as [0] IMPLICIT SET OF — tag 0xA0 wraps the SET body.
|
|
signedAttrsImplicitBytes := ASN1Wrap(0xa0, attrSetBody)
|
|
|
|
// SignatureAlgorithm.
|
|
sigAlg := pkix.AlgorithmIdentifier{Algorithm: signer.SignatureOID()}
|
|
if signer.SignatureOID().Equal(OIDRSAWithSHA256) {
|
|
sigAlg.Parameters = asn1.NullRawValue
|
|
}
|
|
sigAlgBytes := mustMarshal(t, sigAlg)
|
|
|
|
// Signature: OCTET STRING.
|
|
sigOctetBytes := ASN1Wrap(0x04, sig)
|
|
|
|
siBody := append([]byte{}, versionBytes...)
|
|
siBody = append(siBody, sidBytes...)
|
|
siBody = append(siBody, digestAlgBytes...)
|
|
siBody = append(siBody, signedAttrsImplicitBytes...)
|
|
siBody = append(siBody, sigAlgBytes...)
|
|
siBody = append(siBody, sigOctetBytes...)
|
|
siBytes := ASN1Wrap(0x30, siBody)
|
|
|
|
// 5. Build encapContentInfo SEQUENCE { OID data, [0] EXPLICIT OCTET STRING }.
|
|
octetBytes := ASN1Wrap(0x04, encapContent) // OCTET STRING
|
|
encapContentExplicit := ASN1Wrap(0xa0, octetBytes) // [0] EXPLICIT
|
|
oidDataBytes := mustMarshal(t, OIDDataContent)
|
|
encapBody := append([]byte{}, oidDataBytes...)
|
|
encapBody = append(encapBody, encapContentExplicit...)
|
|
encapBytes := ASN1Wrap(0x30, encapBody)
|
|
|
|
// 6. certificates [0] IMPLICIT SET OF Certificate — body is one cert DER.
|
|
certsBytes := ASN1Wrap(0xa0, signerCert.Raw)
|
|
|
|
// 7. digestAlgorithms SET OF AlgorithmIdentifier (one entry).
|
|
digestAlgsBytes := ASN1Wrap(0x31, digestAlgBytes)
|
|
|
|
// 8. signerInfos SET OF SignerInfo (one entry).
|
|
signerInfosBytes := ASN1Wrap(0x31, siBytes)
|
|
|
|
// 9. Assemble SignedData SEQUENCE.
|
|
sdBody := append([]byte{}, []byte{0x02, 0x01, 0x01}...) // version
|
|
sdBody = append(sdBody, digestAlgsBytes...)
|
|
sdBody = append(sdBody, encapBytes...)
|
|
sdBody = append(sdBody, certsBytes...)
|
|
sdBody = append(sdBody, signerInfosBytes...)
|
|
sdSeq := ASN1Wrap(0x30, sdBody)
|
|
|
|
// 10. Wrap as ContentInfo SEQUENCE { OID signedData, [0] EXPLICIT SignedData }.
|
|
contentField := ASN1Wrap(0xa0, sdSeq)
|
|
oidSignedDataDER := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02}
|
|
ciBody := append([]byte{}, oidSignedDataDER...)
|
|
ciBody = append(ciBody, contentField...)
|
|
return ASN1Wrap(0x30, ciBody)
|
|
}
|
|
|
|
// buildSCEPAuthAttrs builds the SET-OF body of SCEP auth-attrs (the bytes
|
|
// inside the [0] IMPLICIT SignedAttrs wrapper). Each Attribute is a SEQUENCE
|
|
// of (OID, SET OF Value); we build them with ASN1Wrap to avoid asn1.Marshal
|
|
// nuances with nested RawValues.
|
|
func buildSCEPAuthAttrs(t *testing.T, msgDigest []byte, messageType domain.SCEPMessageType, transactionID string, senderNonce []byte) []byte {
|
|
t.Helper()
|
|
var out []byte
|
|
// contentType: SET OF OID = SET { OID data }
|
|
out = append(out, attrSeq(t, OIDContentType, ASN1Wrap(0x06, []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}))...)
|
|
// messageDigest: SET OF OCTET STRING
|
|
out = append(out, attrSeq(t, OIDMessageDigest, ASN1Wrap(0x04, msgDigest))...)
|
|
// SCEP messageType: SET OF PrintableString (decimal ASCII)
|
|
out = append(out, attrSeq(t, OIDSCEPMessageType, ASN1Wrap(0x13, []byte(intToAscii(int(messageType)))))...)
|
|
// SCEP transactionID: SET OF PrintableString
|
|
out = append(out, attrSeq(t, OIDSCEPTransactionID, ASN1Wrap(0x13, []byte(transactionID)))...)
|
|
// SCEP senderNonce: SET OF OCTET STRING
|
|
out = append(out, attrSeq(t, OIDSCEPSenderNonce, ASN1Wrap(0x04, senderNonce))...)
|
|
return out
|
|
}
|
|
|
|
// attrSeq builds one Attribute SEQUENCE: SEQUENCE { OID, SET OF value }.
|
|
// The `value` arg is one already-encoded TLV (e.g. an OCTET STRING or
|
|
// PrintableString); attrSeq wraps it in a SET and prefixes the OID.
|
|
func attrSeq(t *testing.T, oid asn1.ObjectIdentifier, value []byte) []byte {
|
|
t.Helper()
|
|
oidBytes := mustMarshal(t, oid)
|
|
setOfValue := ASN1Wrap(0x31, value)
|
|
body := append([]byte{}, oidBytes...)
|
|
body = append(body, setOfValue...)
|
|
return ASN1Wrap(0x30, body)
|
|
}
|
|
|
|
func mustMarshal(t *testing.T, v interface{}) []byte {
|
|
t.Helper()
|
|
out, err := asn1.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("marshal %T: %v", v, err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func intToAscii(i int) string {
|
|
if i == 0 {
|
|
return "0"
|
|
}
|
|
neg := i < 0
|
|
if neg {
|
|
i = -i
|
|
}
|
|
var b []byte
|
|
for i > 0 {
|
|
b = append([]byte{byte('0' + i%10)}, b...)
|
|
i /= 10
|
|
}
|
|
if neg {
|
|
b = append([]byte{'-'}, b...)
|
|
}
|
|
return string(b)
|
|
}
|