diff --git a/Cargo.lock b/Cargo.lock
index 5a7b68e3adcaa0e02321ccc54297bdf49bff7bcf..1d980fea2fa97c456ea3ced9515e83a5254f34a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1265,6 +1265,8 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 [[package]]
 name = "virtue"
 version = "0.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b60dcd6a64dd45abf9bd426970c9843726da7fc08f44cd6fcebf68c21220a63"
 
 [[package]]
 name = "void"
diff --git a/Cargo.toml b/Cargo.toml
index e9a518af1bbb00402d1d4d1f412373b2a61fb713..0eb4833b62b380f9e581bdfe261dfb07faa34925 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,3 @@ debug = 1
 
 [patch."https://github.com/mkj/ed25519-dalek"]
 ed25519-dalek = { path = "/home/matt/3rd/rs/ed25519-dalek" }
-
-[patch.crates-io]
-virtue = { path = "/home/matt/3rd/rs/virtue" }
diff --git a/sshproto/src/packets.rs b/sshproto/src/packets.rs
index 031b167f454bc4d8203348c8d474eb19a0527568..03cc81cbdb39115a7981db44d474e2ffea501152 100644
--- a/sshproto/src/packets.rs
+++ b/sshproto/src/packets.rs
@@ -101,11 +101,11 @@ pub struct UserauthRequest<'a> {
 #[derive(Debug, SSHEncode, SSHDecode)]
 #[sshwire(variant_prefix)]
 pub enum AuthMethod<'a> {
-    #[sshwire(variant = "password")]
+    #[sshwire(variant = SSH_AUTHMETHOD_PASSWORD)]
     Password(MethodPassword<'a>),
-    #[sshwire(variant = "publickey")]
+    #[sshwire(variant = SSH_AUTHMETHOD_PUBLICKEY)]
     PubKey(MethodPubKey<'a>),
-    #[sshwire(variant = "none")]
+    #[sshwire(variant = SSH_NAME_NONE)]
     None,
     #[sshwire(unknown)]
     Unknown(Unknown<'a>),
@@ -228,9 +228,9 @@ pub struct UserauthBanner<'a> {
 #[derive(SSHEncode, SSHDecode, Debug, Clone, PartialEq)]
 #[sshwire(variant_prefix)]
 pub enum PubKey<'a> {
-    #[sshwire(variant = "ssh-ed25519")]
+    #[sshwire(variant = SSH_NAME_ED25519)]
     Ed25519(Ed25519PubKey<'a>),
-    #[sshwire(variant = "ssh-rsa")]
+    #[sshwire(variant = SSH_NAME_RSA)]
     RSA(RSAPubKey<'a>),
     #[sshwire(unknown)]
     Unknown(Unknown<'a>),
@@ -262,9 +262,9 @@ pub struct RSAPubKey<'a> {
 #[derive(Debug, SSHEncode,  SSHDecode)]
 #[sshwire(variant_prefix)]
 pub enum Signature<'a> {
-    #[sshwire(variant = "ssh-ed25519")]
+    #[sshwire(variant = SSH_NAME_ED25519)]
     Ed25519(Ed25519Sig<'a>),
-    #[sshwire(variant = "rsa-sha2-256")]
+    #[sshwire(variant = SSH_NAME_RSA_SHA256)]
     RSA256(RSA256Sig<'a>),
     #[sshwire(unknown)]
     Unknown(Unknown<'a>),
diff --git a/sshproto/src/sshnames.rs b/sshproto/src/sshnames.rs
index f97e01aca4b7ec950b761b1e0d72db7e469c133e..5a535bad754c6937011bd8f80280784e0c17da64 100644
--- a/sshproto/src/sshnames.rs
+++ b/sshproto/src/sshnames.rs
@@ -1,11 +1,6 @@
 //! Named SSH algorithms, methods and extensions. This module also serves as
 //! an index of SSH specifications.
 
-// Note that some names are listed as string literals in packets.rs instead,
-// for #[serde(rename)].  Those could be moved here if this is resolved
-// https://github.com/serde-rs/serde/issues/1964
-// "Rename With Expressions"
-
 /// [RFC8731](https://tools.ietf.org/html/rfc8731)
 pub const SSH_NAME_CURVE25519: &str = "curve25519-sha256";
 /// An older alias prior to standardisation. Eventually could be removed
diff --git a/sshwire_derive/src/lib.rs b/sshwire_derive/src/lib.rs
index 4ca265151d46d26faece369163c1c5bfbb6ea1bd..01709f3b9a4871a44e35f8a6fb0d055582d3c236 100644
--- a/sshwire_derive/src/lib.rs
+++ b/sshwire_derive/src/lib.rs
@@ -3,6 +3,7 @@ use std::collections::HashSet;
 use proc_macro::Delimiter;
 use virtue::generate::FnSelfArg;
 use virtue::parse::{Attribute, AttributeLocation, EnumBody, StructBody};
+use virtue::utils::{parse_tagged_attribute, ParsedAttribute};
 use virtue::prelude::*;
 
 #[proc_macro_derive(SSHEncode, attributes(sshwire))]
@@ -76,58 +77,28 @@ enum FieldAtt {
 }
 
 fn take_cont_atts(atts: &[Attribute]) -> Result<Vec<ContainerAtt>> {
-    atts.iter()
+    let x = atts.iter()
         .filter_map(|a| {
-            match a.location {
-                AttributeLocation::Container => {
-                    let mut s = a.tokens.stream().into_iter();
-                    if &s.next().expect("missing attribute name").to_string()
-                        != "sshwire"
-                    {
-                        // skip attributes other than "sshwire"
-                        return None;
-                    }
-                    Some(if let Some(TokenTree::Group(g)) = s.next() {
-                        let mut g = g.stream().into_iter();
-                        let f = match g.next() {
-                            Some(TokenTree::Ident(l))
-                                if l.to_string() == "no_variant_names" =>
-                            {
-                                Ok(ContainerAtt::NoNames)
-                            }
-
-                            Some(TokenTree::Ident(l))
-                                if l.to_string() == "variant_prefix" =>
-                            {
-                                Ok(ContainerAtt::VariantPrefix)
-                            }
-
-                            _ => Err(Error::Custom {
-                                error: "Unknown sshwire atttribute".into(),
-                                span: Some(a.tokens.span()),
-                            }),
-                        };
+            parse_tagged_attribute(&a.tokens, "sshwire")
+            .transpose()
+        });
 
-                        if let Some(_) = g.next() {
-                            Err(Error::Custom {
-                                error: "Extra unhandled parts".into(),
-                                span: Some(a.tokens.span()),
-                            })
-                        } else {
-                            f
-                        }
-                    } else {
-                        Err(Error::Custom {
-                            error: "#[sshwire(...)] attribute is missing (...) part"
-                                .into(),
-                            span: Some(a.tokens.span()),
-                        })
-                    })
-                }
-                _ => panic!("Non-field attribute for field: {a:#?}"),
-            }
-        })
-        .collect()
+    let mut ret = vec![];
+    // flatten the lists
+    for a in x {
+        for a in a? {
+            let l = match a {
+                ParsedAttribute::Tag(l) if l.to_string() == "no_variant_names" => Ok(ContainerAtt::NoNames),
+                ParsedAttribute::Tag(l) if l.to_string() == "variant_prefix" => Ok(ContainerAtt::VariantPrefix),
+                _ => Err(Error::Custom {
+                    error: "Unknown sshwire atttribute".into(),
+                    span: None,
+                }),
+            }?;
+            ret.push(l);
+        }
+    }
+    Ok(ret)
 }
 
 // TODO: we could use virtue parse_tagged_attribute() though it doesn't support Literals