aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/ocsr/ocsr.go165
-rw-r--r--pkg/ocsr/ocsr_test.go47
2 files changed, 212 insertions, 0 deletions
diff --git a/pkg/ocsr/ocsr.go b/pkg/ocsr/ocsr.go
new file mode 100644
index 0000000..41f4e05
--- /dev/null
+++ b/pkg/ocsr/ocsr.go
@@ -0,0 +1,165 @@
+// Package ocsr creates certificate signing requests for onion addresses. For
+// reference, see RFC 2985 and Appendix B(2) of the CA/Browser BRs.
+package ocsr
+
+import (
+ "crypto"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "encoding/pem"
+ "fmt"
+
+ "sauteed-onions.org/onion-csr/pkg/oaddr"
+)
+
+const (
+ minEntropyBits = 64 // CA/Browser BRs, Appendix B(2)
+ generalNameDNSName = 2 // RFC 5280, §4.2.1.6
+)
+
+var (
+ oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} // RFC 8410, §3
+ oidCASigningNonce = asn1.ObjectIdentifier{2, 23, 140, 41} // CA/Browser BRs, Appendix B(2)
+ oidApplicantSigningNonce = asn1.ObjectIdentifier{2, 23, 140, 42} // CA/Browser BRs, Appendix B(2)
+ oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14} // RFC 2985, Appendix A
+ oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} // RFC 5280, §4.2.1.6.
+)
+
+// New creates a signed onion certification request in PEM format
+func New(priv crypto.Signer, caNonce, applicantNonce []byte) (string, error) {
+ if len(applicantNonce)*8 < minEntropyBits {
+ return "", fmt.Errorf("applicant nonce must be at least %d bits", minEntropyBits)
+ }
+ addr, err := oaddr.NewFromSigner(priv)
+ if err != nil {
+ return "", err
+ }
+ attrs, err := newAttributes(caNonce, applicantNonce, addr.String())
+ if err != nil {
+ return "", err
+ }
+ cr, err := newCertificationRequest(priv, newCertificationRequestInfo(addr, attrs))
+ if err != nil {
+ return "", err
+ }
+ der, err := asn1.Marshal(cr)
+ b := &pem.Block{
+ Type: "CERTIFICATE REQUEST",
+ Bytes: der,
+ }
+ return string(pem.EncodeToMemory(b)), err
+}
+
+// certificationRequestInfo is defined in RFC 2986, §4
+type certificationRequestInfo struct {
+ Version int
+ Subject []pkix.RelativeDistinguishedNameSET
+ SubjectPKInfo subjectPublicKeyInfo
+ Attributes []asn1.RawValue `asn1:"tag:0"`
+}
+
+type subjectPublicKeyInfo struct {
+ Algorithm pkix.AlgorithmIdentifier
+ PublicKey asn1.BitString
+}
+
+func newCertificationRequestInfo(pub [ed25519.PublicKeySize]byte, attrs []asn1.RawValue) certificationRequestInfo {
+ return certificationRequestInfo{
+ Version: 0,
+ Subject: []pkix.RelativeDistinguishedNameSET{},
+ SubjectPKInfo: subjectPublicKeyInfo{
+ Algorithm: pkix.AlgorithmIdentifier{Algorithm: oidEd25519},
+ PublicKey: asn1.BitString{
+ Bytes: pub[:],
+ BitLength: len(pub) * 8,
+ },
+ },
+ Attributes: attrs,
+ }
+}
+
+// newAttributes creates an encoded attribute list with three attributes:
+//
+// - CA signing nonce (CA/B BRs, Appendix B(2))
+// - Applicant signing nonce (CA/B BRs, Appendix B(2))
+// - Requested extensions in the certificate to be issued (PKCS#9). The only
+// requested extension is an onion address as the subject alt name (SAN).
+func newAttributes(caNonce, applicantNonce []byte, onionAddr string) ([]asn1.RawValue, error) {
+ attrCA, err := newAttributeNonce(oidCASigningNonce, caNonce)
+ if err != nil {
+ return nil, err
+ }
+ attrApplicant, err := newAttributeNonce(oidApplicantSigningNonce, applicantNonce)
+ if err != nil {
+ return nil, err
+ }
+ attrReqExt, err := newAttributeRequestedExtensions(onionAddr)
+ if err != nil {
+ return nil, err
+ }
+ return []asn1.RawValue{attrReqExt, attrCA, attrApplicant}, nil
+}
+
+type attribute struct {
+ Type asn1.ObjectIdentifier
+ Value [][]asn1.RawValue `asn1:"set"`
+}
+
+func newAttributeNonce(oid asn1.ObjectIdentifier, nonce []byte) (asn1.RawValue, error) {
+ n := asn1.RawValue{Tag: asn1.TagOctetString, Bytes: nonce}
+ b, err := asn1.Marshal(attribute{oid, [][]asn1.RawValue{[]asn1.RawValue{n}}})
+ return asn1.RawValue{FullBytes: b}, err
+}
+
+func newAttributeRequestedExtensions(onionAddr string) (asn1.RawValue, error) {
+ san, err := newSAN(onionAddr)
+ if err != nil {
+ return asn1.RawValue{}, err
+ }
+ b, err := asn1.Marshal(attribute{oidExtensionRequest, [][]asn1.RawValue{[]asn1.RawValue{san}}})
+ return asn1.RawValue{FullBytes: b}, err
+}
+
+func newSAN(onionAddr string) (asn1.RawValue, error) {
+ b, err := asn1.Marshal([]asn1.RawValue{asn1.RawValue{
+ Class: asn1.ClassContextSpecific,
+ Tag: generalNameDNSName,
+ Bytes: []byte(onionAddr),
+ }})
+ if err != nil {
+ return asn1.RawValue{}, err
+ }
+ b, err = asn1.Marshal(pkix.Extension{
+ Id: oidSubjectAltName,
+ Value: b,
+ })
+ return asn1.RawValue{FullBytes: b}, err
+}
+
+// certificationRequest is defined in RFC 2986, §4
+type certificationRequest struct {
+ CertificationRequestInfo certificationRequestInfo
+ SignatureAlgorithm pkix.AlgorithmIdentifier
+ SignatureValue asn1.BitString
+}
+
+func newCertificationRequest(priv crypto.Signer, cri certificationRequestInfo) (certificationRequest, error) {
+ msg, err := asn1.Marshal(cri)
+ if err != nil {
+ return certificationRequest{}, err
+ }
+ sig, err := priv.Sign(rand.Reader, msg, crypto.Hash(0))
+ if err != nil {
+ return certificationRequest{}, err
+ }
+ return certificationRequest{
+ CertificationRequestInfo: cri,
+ SignatureAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidEd25519},
+ SignatureValue: asn1.BitString{
+ Bytes: sig,
+ BitLength: len(sig) * 8,
+ },
+ }, nil
+}
diff --git a/pkg/ocsr/ocsr_test.go b/pkg/ocsr/ocsr_test.go
new file mode 100644
index 0000000..aea2c97
--- /dev/null
+++ b/pkg/ocsr/ocsr_test.go
@@ -0,0 +1,47 @@
+package ocsr
+
+import (
+ "crypto"
+ "testing"
+
+ "sauteed-onions.org/onion-csr/internal/testonly"
+)
+
+func TestNew(t *testing.T) {
+ // test params that HARICA were able to verify successfully with address
+ // b3ttntbojgjyj54nbjrvcabqftyti5m6p6vebkqmwvbbxydvevkhp3ad.onion
+ testPriv := testonly.Ed25519Priv(t, "797134fa0f5667479138d868f258c9b81ef9a247fb2cfbfaec52e2afca0ff457")
+ testCANonce := testonly.DecodeHex(t, "4865685837665A75714B655361367A6352493242")
+ testApplicantNonce := testonly.DecodeHex(t, "00000000000000000000")
+ testPEM := `-----BEGIN CERTIFICATE REQUEST-----
+MIIBFzCBygIBADAAMCowBQYDK2VwAyEADuc2zC5Jk4T3jQpjUQAwLPE0dZ5/qkCq
+DLVCG+B1JVSggZYwWgYJKoZIhvcNAQkOMU0wSzBJBgNVHREEQjBAgj5iM3R0bnRi
+b2pnanlqNTRuYmpydmNhYnFmdHl0aTVtNnA2dmVia3Ftd3ZiYnh5ZHZldmtocDNh
+ZC5vbmlvbjAgBgRngQwpMRgwFgQUSGVoWDdmWnVxS2VTYTZ6Y1JJMkIwFgYEZ4EM
+KjEOMAwECgAAAAAAAAAAAAAwBQYDK2VwA0EALWQAfPUyaiGi5DriKQBijomZik+L
+mEi8egO6VcgM2Q6RSajveWx5EImi3nQcU/vZ2NhdzYRyuiG1zYcj8SlgBA==
+-----END CERTIFICATE REQUEST-----
+`
+ for _, table := range []struct {
+ desc string
+ priv crypto.Signer
+ caNonce []byte
+ applicantNonce []byte
+ want string
+ }{
+ {"invalid: short nonce", testPriv, testCANonce, testonly.DecodeHex(t, "01020304050607"), ""},
+ {"invalid: private key", testonly.RSAPriv(t), testCANonce, testApplicantNonce, ""},
+ {"valid", testPriv, testCANonce, testApplicantNonce, testPEM},
+ } {
+ csr, err := New(table.priv, table.caNonce, table.applicantNonce)
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := csr, table.want; got != want {
+ t.Errorf("%s: got csr\n%s\nbut wanted\n%s", table.desc, got, want)
+ }
+ }
+}